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.