Skip to main content

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.

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

 

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.

 


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


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


 

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


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


@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 , 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.


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.


Your APIUser, what privileges does the account require?


Your APIUser, what privileges does the account require?


Privilege Requirements (jamf.com)


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.


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" : "deviceIdse0]"

} ]

} % 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"

}

 


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" : "deviceIdse0]"

} ]

} % 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"

}

 


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

 

 

#!/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

 

 

 


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.


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


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


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.


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


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.


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. 


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

 

 

#!/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?


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?


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

 

 

 

 


@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!


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}"

 


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


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"


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"


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


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}"

 


Nice work. Thank you for helping me improve this. 


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


ok I'll keep poking around.


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.


Reply