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

 

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


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


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


I am seeing a hiccup here when running the script:

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

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

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

 echo 'User clicked Start Now'

User clicked Start Now

+ echo 'Initializing Software Update'

Initializing Software Update

+ rm -rf /Users/dvasqu29/logs/

+ launchctl /private/tmp/osUpdate.plist

Unrecognized subcommand: /private/tmp/osUpdate.plist

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

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

within that domain. The available specifier forms are:

 

system/[service-name]

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

to make modifications.

 

user/<uid>/[service-name]

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

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

exist on iOS.

 

gui/<uid>/[service-name]

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

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

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

 

session/<asid>/[service-name]

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

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

domain.

 

pid/<pid>/[service-name]

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

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

 

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

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

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

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

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

 

 


I am seeing a hiccup here when running the script:

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

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

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

 echo 'User clicked Start Now'

User clicked Start Now

+ echo 'Initializing Software Update'

Initializing Software Update

+ rm -rf /Users/dvasqu29/logs/

+ launchctl /private/tmp/osUpdate.plist

Unrecognized subcommand: /private/tmp/osUpdate.plist

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

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

within that domain. The available specifier forms are:

 

system/[service-name]

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

to make modifications.

 

user/<uid>/[service-name]

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

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

exist on iOS.

 

gui/<uid>/[service-name]

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

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

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

 

session/<asid>/[service-name]

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

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

domain.

 

pid/<pid>/[service-name]

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

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

 

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

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

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

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

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

 

 


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


I am seeing a hiccup here when running the script:

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

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

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

 echo 'User clicked Start Now'

User clicked Start Now

+ echo 'Initializing Software Update'

Initializing Software Update

+ rm -rf /Users/dvasqu29/logs/

+ launchctl /private/tmp/osUpdate.plist

Unrecognized subcommand: /private/tmp/osUpdate.plist

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

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

within that domain. The available specifier forms are:

 

system/[service-name]

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

to make modifications.

 

user/<uid>/[service-name]

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

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

exist on iOS.

 

gui/<uid>/[service-name]

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

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

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

 

session/<asid>/[service-name]

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

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

domain.

 

pid/<pid>/[service-name]

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

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

 

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

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

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

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

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

 

 


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


I am seeing a hiccup here when running the script:

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

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

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

 echo 'User clicked Start Now'

User clicked Start Now

+ echo 'Initializing Software Update'

Initializing Software Update

+ rm -rf /Users/dvasqu29/logs/

+ launchctl /private/tmp/osUpdate.plist

Unrecognized subcommand: /private/tmp/osUpdate.plist

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

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

within that domain. The available specifier forms are:

 

system/[service-name]

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

to make modifications.

 

user/<uid>/[service-name]

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

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

exist on iOS.

 

gui/<uid>/[service-name]

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

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

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

 

session/<asid>/[service-name]

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

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

domain.

 

pid/<pid>/[service-name]

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

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

 

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

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

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

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

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

 

 


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


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

 

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


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

 

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


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


So we are struggling also with the macOS updates 

This script looks somewhat helpful. 

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

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

 

Is this possible?


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

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


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

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


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


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


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


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


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

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


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


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


So we are struggling also with the macOS updates 

This script looks somewhat helpful. 

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

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

 

Is this possible?


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


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


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

 

I will test again. 


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

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


You misunderstood me :) 

I am using your script but other then a warning (which will stay in view for 30-60 minutes) there is no warning when the restart actually takes place. So it is still a restart without a short notice warning. 

 


You misunderstood me :) 

I am using your script but other then a warning (which will stay in view for 30-60 minutes) there is no warning when the restart actually takes place. So it is still a restart without a short notice warning. 

 


Yep, nothing you can do about that. That's the main downfall of MDM commands in my opinion. Word is, Apple is release more features to make the update process better though. So stay hopeful.

 


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

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


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


I'm finding most of these scripts are performing a serial number search to find the computer id, which will be the bit that taxes the server, I haven't found where it is stored yet normally, but I have noticed that 'jamf recon' outputs the computer id, so I realised if I capture that and store it, we can skip the S/N search, grab the stored computer id, and get right on with sending the push command.


@wakco11 , when you have a moment, please post your process. Any help improving the script is much appreciated. Thanks!


 

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


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! 


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


@wakco11 , when you have a moment, please post your process. Any help improving the script is much appreciated. Thanks!


Step 1: Grab the computer_id from a recon.

Like most of us we need to have Jamf getting inventory updates from computers. We have a large number of laptops, which causes a common issue of network errors being reported if we use the standard basic inventory update policy, as users on laptops often disconnect their laptop from the network in the middle of the inventory update. I resolved this by using the hidden command jamf scheduledTask to create a launchd plist in /Library/LaunchDaemons that would trigger the jamf recon similar to a check-in, so the logs of network errors end up in the computers system log instead of causing jamf to log failed inventory update attempts, when a laptop happens to be disconnected during an inventory update. I realised I could change this to a script that could capture the jamf recon output and store it somewhere i.e.

 

#!/bin/sh
# Create script
SCRIPTFILE="/Library/Scripts/InventoryScript"
MYSCRIPT="#!/bin/sh
# This is a simple script to perform an inventory update, and save the computer id
RECON=\\"\\$(/usr/local/bin/jamf recon -randomDelaySeconds 450)\\"
if [[ \\"\\$RECON\\" = *\\"computer_id\\"* ]]; then
echo \\"\\$RECON\\" | grep computer_id | grep -o '[0-9]\\\\+' > '/Library/Application Support/JAMF/computer_id'
fi
"
echo "$MYSCRIPT" > "$SCRIPTFILE"
# Give it execute permissions
chmod ugo+x "$SCRIPTFILE"
# Make it a scheduled task, running once a day (1440 Minutes = 24 hours)
/usr/local/bin/jamf scheduledTask -command "$SCRIPTFILE" -name recon -runAtLoad -minute '*/1440/'
# Let jamf know it is all done
ps -axj | grep jamf
ls -l "$SCRIPTFILE"
ls -l /Library/LaunchDaemons/com.jamfsoftware.task.recon.plist

 

This would cause an inventory update once a day, and within 10 minutes of a computer starting up (450 seconds is about 7 minutes 30 seconds). and leaves the Jamf Computer ID stored in /Library/Application Support/JAMF/computer_id, as captured from the last line of jamf recon which tends to look like <computer_id>1234</computer_id> where 1234 is the Jamf Computer ID.

 

Step 2: Used the stored computer_id to send a push command. 

Replacing:

 

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

 

With:

 

deviceID=$(cat '/Library/Application Support/JAMF/computer_id')

 

And removing the Serial Number collection.

 

I am actually doing more than that, I'm checking the file exists, and forcing an inventory update to capture it again if it isn't there, but this provides the basic difference.

 

- Richard


Step 1: Grab the computer_id from a recon.

Like most of us we need to have Jamf getting inventory updates from computers. We have a large number of laptops, which causes a common issue of network errors being reported if we use the standard basic inventory update policy, as users on laptops often disconnect their laptop from the network in the middle of the inventory update. I resolved this by using the hidden command jamf scheduledTask to create a launchd plist in /Library/LaunchDaemons that would trigger the jamf recon similar to a check-in, so the logs of network errors end up in the computers system log instead of causing jamf to log failed inventory update attempts, when a laptop happens to be disconnected during an inventory update. I realised I could change this to a script that could capture the jamf recon output and store it somewhere i.e.

 

#!/bin/sh
# Create script
SCRIPTFILE="/Library/Scripts/InventoryScript"
MYSCRIPT="#!/bin/sh
# This is a simple script to perform an inventory update, and save the computer id
RECON=\\"\\$(/usr/local/bin/jamf recon -randomDelaySeconds 450)\\"
if [[ \\"\\$RECON\\" = *\\"computer_id\\"* ]]; then
echo \\"\\$RECON\\" | grep computer_id | grep -o '[0-9]\\\\+' > '/Library/Application Support/JAMF/computer_id'
fi
"
echo "$MYSCRIPT" > "$SCRIPTFILE"
# Give it execute permissions
chmod ugo+x "$SCRIPTFILE"
# Make it a scheduled task, running once a day (1440 Minutes = 24 hours)
/usr/local/bin/jamf scheduledTask -command "$SCRIPTFILE" -name recon -runAtLoad -minute '*/1440/'
# Let jamf know it is all done
ps -axj | grep jamf
ls -l "$SCRIPTFILE"
ls -l /Library/LaunchDaemons/com.jamfsoftware.task.recon.plist

 

This would cause an inventory update once a day, and within 10 minutes of a computer starting up (450 seconds is about 7 minutes 30 seconds). and leaves the Jamf Computer ID stored in /Library/Application Support/JAMF/computer_id, as captured from the last line of jamf recon which tends to look like <computer_id>1234</computer_id> where 1234 is the Jamf Computer ID.

 

Step 2: Used the stored computer_id to send a push command. 

Replacing:

 

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

 

With:

 

deviceID=$(cat '/Library/Application Support/JAMF/computer_id')

 

And removing the Serial Number collection.

 

I am actually doing more than that, I'm checking the file exists, and forcing an inventory update to capture it again if it isn't there, but this provides the basic difference.

 

- Richard


Fancy stuff man. I like it.


Reply