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

bwoods
Valued Contributor

One note, the section below can mostly likely be turned into a variable or parameter to edit the version number on the fly in the future. Otherwise, have at it. Help me improve this thing.

bwoods_0-1652730788625.png

 

I seem to be running this script on machines that need an update and it does not successfully update the machines:

 

Here is the output from one:

 

 

[STEP 1 of 5]
Executing Policy Self Service: Install macOS Updates (M1)
[STEP 2 of 5]
Running script MacOS updater script (M1)...
Script exit code: 0
Script result:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   392    0   392    0     0   2230      0 --:--:-- --:--:-- --:--:--  2333
eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiNTBkYmEwNmYtZjdiOC00YzE3LWFkYjUtZjg2MjM5MmVhMDYwIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiIxOSIsImV4cCI6MTY1MzY3MTAxNX0.RqAJD2DksSq7N2Du9-taFtvtD9wTBVdrZpXOd4fbIcY
388
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   394    0   224  100   170   1399   1062 --:--:-- --:--:-- --:--:--  2662
{
  "responses" : [ {
    "id" : "c93d3095-125d-42a3-a58c-94f54aef980e",
    "href" : "https://mycompany.jamfcloud.com/mycompany/api/v1/mdm/commands?uuids=c93d3095-125d-42a3-a58c-94f54aef980e"
  } ],
  "errors" : [ ]
}  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   391    0   391    0     0   3715      0 --:--:-- --:--:-- --:--:--  4296
{
  "token" : "eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiNTBkYmEwNmYtZjdiOC00YzE3LWFkYjUtZjg2MjM5MmVhMDYwIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiIxOSIsImV4cCI6MTY1MzY3MTAxNn0.nDEK6XdTEm5BtXYIse0o53Oob3lo8SPT9_N5p0lu7Fo",
  "expires" : "2022-05-27T17:03:36.42Z"

 

SMR1
Contributor II

I ran the above script on one of our M1's and it says completed with the below details. The user's MacBook was locked.

Script result: Serial number: XYVH2K0FVH
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 391 0 391 0 0 905 0 --:--:-- --:--:-- --:--:-- 922 Auth Token: { "token" : "eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiNmQwNDQwMmQtZjgyOC00Mzk2LWEwNjItNjRkOWQ2NjU4ZTJmIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiIxIiwiZXhwIjoxNjU5MjIwMTkxfQ.IMGqlh89pAIIGNOI0Fnn4bKKLkZwZb__G6b4p1nH6Z0", "expires" : "2022-07-30T22:29:51.103Z" } Token: eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiNmQwNDQwMmQtZjgyOC00Mzk2LWEwNjItNjRkOWQ2NjU4ZTJmIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiIxIiwiZXhwIjoxNjU5MjIwMTkxfQ.IMGqlh89pAIIGNOI0Fnn4bKKLkZwZb__G6b4p1nH6Z0 Device ID: 107 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 392 0 224 100 168 539 404 --:--:-- --:--:-- --:--:-- 982 { "responses" : [ { "id" : "e84835e9-7ee3-4884-9c01-2a032512cbfb", "href" : "https://lplfinancial.jamfcloud.com/lplfinancial/api/v1/mdm/commands?uuids=e84835e9-7ee3-4884-9c01-2a..." } ], "errors" : [ ] } % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 391 0 391 0 0 854 0 --:--:-- --:--:-- --:--:-- 884 { "token" : "eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiNmQwNDQwMmQtZjgyOC00Mzk2LWEwNjItNjRkOWQ2NjU4ZTJmIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiIxIiwiZXhwIjoxNjU5MjIwMTkzfQ.cpkwPxLRneGh3uI9THNzys7rl8cHjFnEEmu8sTevuoA", "expires" : "2022-07-30T22:29:53.276Z" }

nwagner
Contributor

I will def be trying this out as soon as possible. Awesome work @bwoods !!

bwoods
Valued Contributor

 

Thanks @nwagner! Let me know how your testing goes.

moussabl
New Contributor II

Hi @bwoods. Firstly, thank you for your work on this. I tested your script with a Jamf admin account in the Part B policy and seems to work really well! My question, do you know what privileges are needed for a non administrator Jamf account specifically to able to run this task?  

Cheers! 

@moussabl Here are the JAMF account permissions I use for this script and it works for me.  

Account
Access Level - Full Access
Privilege Set - Custom


Privileges (everything is unchecked except for the following):
JAMF Pro Server Objects - Computers - Create+Read

JAMF Pro Server Actions - Send Computer Remote Command to Download and Install macOS Update - Checked

moussabl
New Contributor II

Awesome, Thank you @stutz 

sdagley
Honored Contributor III

@bwoods Thanks for posting the example. Since Jamf discourages calling the API from an arbitrary endpoint, and recommends it's better used from an administrator's system you might consider creating a script that uses the API to get the members of a Static or Smart Group as the target machines for an update, and then makes the send-updates endpoint API call to force the update for those machines.

bwoods
Valued Contributor

@sdagley , good to know. I thought that wasn't an issue with the new bearer token feature. That's definitely something worth looking into. Thanks again.

sdagley
Honored Contributor III

Bearer token auth is an improvement if you're making repeated API calls, but it does still require some exposure of the credentials used to request the token.

cbruce
New Contributor III

Your APIUser, what privileges does the account require?

McAwesome
Contributor III

This function is going to be so helpful.

Out of curiosity, are you checking before running to see if the update is already installed?  I know Jamf inventory is not always the most reliable.  In the past we've checked by running softwareupdate -l and checking for the phrase "found the following new or updated software" in the results.  I think it still works on 11+, but with how Apple is treating that command I should probably try to get the MDM command version of that same idea up and running.

bwoods
Valued Contributor

@McAwesome I downgrade my test machine OS before I run this. This currently works reliably to update M1's running any version of Monterey, but I'm currently working on getting full OS Upgrades working. For instance, upgrading from Big Sur to Monterey.

Right what I'm meaning is a way to make sure the function needs to be called at all.  This kind of a function is likely to be used after some kind of pop up warning given to the end user.  No sense showing them a countdown if they're already up to date.

bwoods
Valued Contributor

Ah okay, I see what you're saying. Jamf Pro doesn't always show the correct OS version, so I need to check that the reported version is actually the version on the client. 

These couple of functions may help you add some safety checks in.

batteryCheck(){
	powerType=$(pmset -g batt | head -n 1 | cut -c19- | rev | cut -c 2- | rev)

	if [[ "$powerType" == "Battery Power" ]]; then
    	echo "Machine is on Battery Power."
		bat=$(pmset -g batt | grep 'InternalBattery' | awk '{print $3}' | tr -d '%'';')
        if (( $bat > 60 )); then
			echo "Battery Level OK, Continuing Update..."
		else
        	echo "Battery Level Insufficient for this update"
            return 1
		fi
	else
    	echo "Machine is on AC Power."
	fi
}

bootstrapTokenCheck(){
	bootstrap=$(profiles status -type bootstraptoken)
	if [[ $bootstrap == *"escrowed to server: YES"* ]]; then
		echo "Bootstrap escrowed"
	else
		echo "Bootstrap not escrowed.  Cannot update with MDM commands."
		return 1
	fi
}

freeSpaceRequirement(){
	freespace=$(df -h -m | grep -m 1 /System/Volumes/Data | awk '{print $4}')
    if [[ $freespace -lt 15000 ]]; then
    	echo "Insufficient free space"
		return 1
	else
		echo "Machine has at least 15 GBs of free space"
	fi
}

verifyUpdateNeeded(){
	# Check for available updates
    availableUpdates=`/usr/libexec/mdmclient AvailableOSUpdates`
    
    # Updates available
    if [[ "$availableUpdates" == *"=== OS Update Item ==="* ]]; then
    	echo "Updates Available"
    # Updates not available
    else
    	echo "No Updates Available"
        return 1
	fi
}

# Verifying update is needed and requirements have been met.
verifyUpdateNeeded || exit 0
bootstrapTokenCheck || exit 0
freeSpaceRequirement || exit 0
batteryCheck || exit 0

 Also if I'm not mistaken you can streamline your update command so you don't have to fiddle with it as much next time around.  If you don't specify a specific version, it will download the newest one based on device eligibility.

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}\"],\"skipVersionVerification\":false,\"applyMajorUpdate\":false,\"updateAction\":\"DOWNLOAD_AND_INSTALL\",\"forceRestart\":true}"

 

bwoods
Valued Contributor

Nice work. Thank you for helping me improve this. 

tkimpton
Valued Contributor II

can the bootstrap token check be skipped if not using mobile accounts? or is it a requirement that would stop it working?

sdagley
Honored Contributor III

@tkimpton The account type doesn't matter, a bootstrap token is required for any MDM triggered updates

Gary_R
New Contributor II

Thanks again

Testing also seeing how the schema\parameters of the post on the mdm command can mirror this check box "Include major updates, if available"Screen Shot 2022-05-21 at 9.58.05 AM.png

bwoods
Valued Contributor

@Gary_R , I found that full OS Upgrades can be completed with the configuration you posted above, but it doesn't work if you specify a version. I've only been able to do this with the Jamf Pro GUI though. Can't seem to get it working with the API.

Gary_R
New Contributor II

ok I'll keep poking around.

markopolo
Contributor

Hmm, I can't seem to get this working. Here is my output:

Script result:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   395    0   395    0     0    578      0 --:--:-- --:--:-- --:--:--   583
eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiZWNkNzkyMTctMDYzMy00YjcxLWFmMjItOGE5YzViYTljZjhkIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiI2NSIsImV4cCI6MTY1MjkxNDE4MH0.etverG0GbjaH6XAgPtdYYtVLlIJtYdBOg-dOds4uh_0
-:1: parser error : StartTag: invalid element name
<!doctype html><html lang="en"><head><title>HTTP Status 404 – Not Found</title
 ^
-:1: parser error : Extra content at the end of the document
<!doctype html><html lang="en"><head><title>HTTP Status 404 – Not Found</title
 ^

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   372    0   206  100   166    415    334 --:--:-- --:--:-- --:--:--   767
{

  "httpStatus" : 400,

  "errors" : [ {

    "code" : "INVALID_ID",

    "description" : "id field must be string of positive numeric value or -1",

    "id" : "",

    "field" : "deviceIds[0]"

  } ]

}  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   395    0   395    0     0    782      0 --:--:-- --:--:-- --:--:--   796
{

  "token" : "eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiZWNkNzkyMTctMDYzMy00YjcxLWFmMjItOGE5YzViYTljZjhkIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiI2NSIsImV4cCI6MTY1MjkxNDE4Mn0.FQmrKKDv02zotLXD-T2l7nefutUZPvNvLvYsfYLwUW4",

  "expires" : "2022-05-18T22:49:42.198Z"

}

 

bwoods
Valued Contributor

@markopolo it looks like your device id isn't being generated for some reason. Run my function below to test recovering the device id. Refer to the highlighted sections as they determine how the device id is generated.

It looks like you are generating a token properly though. 

bwoods_0-1652928362837.png

 

 

#!/bin/bash

# Server connection information
URL=""
username=""
password=""

# 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)
	
	# 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 "My Device ID is ${deviceID}"
}

initializeSoftwareUpdate 

 

 

 

Thanks, I wish I was better at this :). I do appreciate the help...  

So if I reduce the script to this:

#!/bin/bash

# Server connection information
URL="xxxxxxxxxxx"
username="xxxxxxxxxxx"
password="xxxxxxxxxxx"

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

## Generate new auth token
token=$(curl -X POST -u "${username}:${password}" -s ${URL}/api/v1/auth/token | plutil -extract token raw –)

# 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()' -)

This is what I get:

D0Q9P63VFM
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    45  100    45    0     0     94      0 --:--:-- --:--:-- --:--:--    97


-:10: parser error : Opening and ending tag mismatch: br line 8 and p
</p>
    ^
-:11: parser error : Opening and ending tag mismatch: p line 8 and body
</body>
       ^
-:12: parser error : Opening and ending tag mismatch: body line 5 and html
</html>
       ^
-:12: parser error : Premature end of data in tag html line 1
</html>
       ^

 And if I use a "curl -s -X" instead of a "curl -s -H" I get this:

D0Q9P63VFM
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    45  100    45    0     0     94      0 --:--:-- --:--:-- --:--:--    96


-:1: parser error : StartTag: invalid element name
<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</**bleep**
 ^
-:1: parser error : Extra content at the end of the document
<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</**bleep**
 ^

 Not sure what I'm doing wrong. Could there be something screwy with the API on my on-prem JSS?

bwoods
Valued Contributor

@markopolo , you need the variables below to properly generate a bearer token. Otherwise your API call will fail. First practice generating a token, then try to get the device id. I suggest running this in a code editor like "CoderRunner" first.

Basically, there is a difference between the "authToken" and "token" variables. The "authToken" must be generated first, but contains unnecessary information like the expiration date. The "token" variable gets rid of the unnecessary information and can be used to properly make the API call.

I would also test running my original code above with your api account, then testing with your admin account. If you get different results, it's a permissions issue.

 

 

 

 

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

 

 

 

 

Thanks, I actually got it working last night (tried so many things I don't remember what fixed it). Here is the final script I'm using:

#!/bin/bash

# Server connection information
URL="xxxxxxxx"
username="xxxxxxxx"
password="xxxxxxxx"

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

echo "Serial number: ${serialNumber}"

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: ${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 "Device ID: ${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.4\",\"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

 Thanks for your help, I really appreciate it!

For this to work, does the user have to be logged on. I tested it on one our users, but the device was locked and it never updated.

Gary_R
New Contributor II

This is really solid.  Thanks for your work on this.

As an FYI unless you want to maneuver around password complexity I experienced issues the % symbols in the API account and passing that from the script.  I created a new account without it.

bwoods
Valued Contributor

Thank you. You can use single quotes to get around the password issue as well. 

awginger
Contributor

Hey @bwoods, thanks for sharing this. I am just testing now and it seems great!

I have version part so that machines just get the latest available update. I have found that the process is a bit abrupt though, Have you found anyway to warn users that their Mac is about to reboot to start the update?

I use a user interaction to let them know that updates are required but because the update takes a good while to download (especially 12.3.1 to 12.4) the fact that it just restarts without another warning is a bit unfriendly.

Be great to know what you, or others, have done to make this a better experience for users.

bwoods
Valued Contributor

@awginger , I use User Interaction in combination with jamf helper/launch daemon prompts to notify them about the reboot. The last prompt basically tells them that the computer will randomly reboot.

If Apple just fixed some basic UX prompts this would be perfect.

bwoods_0-1653493204522.png

bwoods_1-1653493227870.png

bwoods_2-1653493237314.png

 

@bwoods I was thinking along the same lines and have put a Jamf helper message like yours into the workflow. I think it will be something that we need to communicate to users as the policy we use now (using user interaction) warns them that they have 5mins to restart but the 'download and install' option doesn't appear to give us this flexibility. It would be good if Jamf or Apple give us some more user friendly ways to do this in the future.

Great work on the script!

bwoods
Valued Contributor

@awginger Apple included the max deferral limit, but it doesn't force the reboot after the limit is reached. They just need to do that last thing to make all of our lives easier.  

What do you have setup for your jamfhelper window to popup like that and give a countdown and remaining deferral attempts? Could you share it?

I'm not them, but in my test version here's what I used for the prompt when I was testing with it

# JamfHelper window options
heading="macOS Software Update"
description=$( echo "Your computer has mandatory macOS updates it needs to install.\n\nThese updates could take up to an hour to install.")
timeout="3600"
canCancel=$(echo "$7" | tr '[:upper:]' '[:lower:]')
style="hud"
button1="Start Now"
icon="/Applications/Software Center.app/Contents/Resources/AppIcon.icns"
currentUser=$(who | grep console | grep -v _mbsetupuser)

countdown(){
    # Gets current time. Script is logging how many seconds the prompt was displayed for.
    # Primarily used for record keeping.
    promptTime=`date +%s`
	returned=`/Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper \
		-windowType $style \
		-title "Software Center" \
		-heading "$heading" \
		-description "$description" \
		-icon "$icon" \
		-button1 "$button1" \
		-button2 "Later" \
		-cancelButton 2 \
		-defaultButton 1 \
		-lockHUD \
		-alighDescription left \
		-timeout $timeout -countdown`

    readTime=$(($(date +%s) - $promptTime))

    # Log how quickly the user made a selection.
    echo "Countdown window was up for $(($readTime/60)) minutes $(($readTime%60)) seconds."
}

countdownCancelCheck(){
	# They started the policy
	if [[ $returned -eq 0 ]]; then
		echo "User has chosen to start the update now."
	elif [[ $returned -eq 2 ]]; then
		echo "User has cancelled for now."
        return 1
	else
		echo "Unknown error.  JamfHelper may have been force quit."
		echo "Assuming user meant to cancel."
        return 1
	fi
}

countdownForced(){
    # Gets current time. Script is logging how many seconds the prompt was displayed for.
    # Primarily used for record keeping.
    promptTime=`date +%s`

	/Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper \
		-windowType $style \
		-title "Software Center" \
		-heading "$heading" \
		-description "$description" \
		-icon "$icon" \
		-button1 "$button1" \
		-defaultButton 1 \
		-lockHUD \
		-alighDescription left \
		-timeout $timeout -countdown

    readTime=$(($(date +%s) - $promptTime))

    # Log how quickly the user made a selection.
    echo "Countdown window was up for $(($readTime/60)) minutes $(($readTime%60)) seconds."
}

curtainPuller(){
            /Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper \
            -windowType fs \
            -title "Software Update" \
            -heading "$heading" \
            -description "$description" \
            -icon "$icon" >/dev/null 2>&1 &
}

if [ "$currentUser" == "" ]; then
	echo "No one signed in.  Proceeding with update."
elif [ "$canCancel" == "yes" ]; then
	echo "$currentUser is signed in and allowed to cancel"
    countdown
    countdownCancelCheck || exit 0
else
	echo "$currentUser is signed in and update is mandatory"
    countdownForced
fi
curtainPuller

These functions give some flexibility on allowing the user to cancel (set by $7) and also pulling up a full screen window so they don't keep working while the update is installing.  I also posted a couple safety check functions earlier that can help avoid unneeded interruptions to the end user if they either already updated or can't actually update.

Hey McAwesome,

 

Thanks this is very helpful. I'm looking more for the solution that bwoods implemented though where its just a small window.

 

I like how he has three different popups: The deferral on and then the one sayings it's started. 

 

Honestly i'd like an exact copy of his script.