05-16-2022 12:34 PM - edited 06-23-2022 08:47 AM
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
Posted on 05-27-2022 09:23 AM
Hey @bwoods would you be able to share your jamfhelper script you wrote?
05-31-2022 06:40 AM - edited 05-31-2022 06:48 AM
@MPL , Below is the first part of my jamf helper process. It is a launch daemon that runs a custom policy. The deferral interval can be changed to what ever you want with the built in parameter.
#!/bin/bash
deferInterval="$4"
createLaunchDaemon(){
echo "<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.os.update</string>
<key>UserName</key>
<string>root</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/jamf/bin/jamf</string>
<string>policy</string>
<string>-event</string>
<string>updateOS</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>${deferInterval}</integer>
</dict>
</plist>" > /tmp/osUpdate.plist
sudo chown root:wheel /tmp/osUpdate.plist
sudo chmod 755 /tmp/osUpdate.plist
sudo launchctl load /tmp/osUpdate.plist
}
createLaunchDaemon
05-31-2022 06:43 AM - edited 06-17-2022 09:03 AM
@MPL Below is the second part of my jamf helper process. It's basically a set of dialogs that guides the user through the updates. logs are created to track how many times the user has deferred the prompt. Simply create a custom policy with the trigger "updateOS". The launch daemon in part A will run this at the desired time interval.
#!/bin/bash
# Server connection information
URL="https://url.jamfcloud.com"
username="$4"
password="$5"
# Operatitng system information
osVersion="$6"
osName="$7"
osFullName="$7 "${osVersion}""
serialNumber=$(system_profiler SPHardwareDataType | awk '/Serial Number/{print $4}')
# Log information
currentUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )
logFolder="/Users/"${currentUser}"/logs/"
deferlog="${logFolder}/deferlog.txt"
lastDeferralCount=$(cat "${deferlog}")
# Jamf helper information
jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"
description1="${osFullName} is now available. Select 'Start Now' to initialize the update. Select 'Defer' to complete the update at another time.
Once the update is initiated, it may take 25-60 minutes for the computer to restart.
Remaining Deferral Attempts: 3"
description2="${osFullName} is now available. Select 'Start Now' to initialize the update. Select 'Defer' to complete the update at another time.
Once the update is initiated, it may take 25-60 minutes for the computer to restart.
Remaining Deferral Attempts: 2"
description3="${osFullName} is now available. Select 'Start Now' to initialize the software update. Select 'Defer' to initialize the update at another time.
Once the update is initiated, it may take 25-60 minutes for the computer to restart.
Remaining Deferral Attempts: 1"
finalDescription="The maximum deferral limit has been reached. Select 'Start Now' to initialize the ${osFullName} update. Otherwise, use the remaining time to sync any unsaved changes to OneDrive.
Remaining Deferral Attempts: 0"
waitDescription="Processing software update. Please wait for the computer to restart. Ensure that the power adapter is connected. This process may take awhile."
timeout="$8"
########Functions###########
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\":\"${osVersion}\",\"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}"
}
pleaseWait(){
buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "$waitDescription" -alignDescription "Left")
}
firstPrompt(){
# First Prompt
buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "${description1}" -alignDescription "Left" -button1 "Start Now" -button2 "Defer")
echo $buttonClicked
if [[ $buttonClicked == 0 ]]; then
echo "User clicked Start Now"
echo "Initializing Software Update"
rm -rf "${logFolder}"
launchctl unload /private/tmp/osUpdate.plist
rm -rf /private/tmp/osUpdate.plist
initializeSoftwareUpdate
pleaseWait
elif [[ $buttonClicked == 2 ]]; then
# Declare what the user clicked
echo "User Clicked Defer"
# Set deferralCount to 0
deferralCount="1"
#Append current number of deferrals to deferlog.txt
echo $deferralCount > $deferlog
exit 0
fi
}
secondPrompt(){
# Second Prompt
buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "${description2}" -alignDescription "Left" -icon -button1 "Start Now" -button2 "Defer")
echo $buttonClicked
if [[ $buttonClicked == 0 ]]; then
echo "User clicked Start Now"
echo "Initializing Software Update"
rm -rf "${logFolder}"
launchctl unload /private/tmp/osUpdate.plist
rm -rf /private/tmp/osUpdate.plist
initializeSoftwareUpdate
pleaseWait
elif [[ $buttonClicked == 2 ]]; then
echo "User Clicked Defer"
# Set deferralCount to 0
deferralCount="2"
#Append current number of deferrals to deferlog.txt
echo $deferralCount > $deferlog
exit 0
fi
}
thirdPrompt(){
# Third Prompt
buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "${description3}" -alignDescription "Left" -button1 "Start Now" -button2 "Defer")
echo $buttonClicked
if [[ $buttonClicked == 0 ]]; then
echo "User clicked Start Now"
echo "Initializing Software Update"
rm -rf "${logFolder}"
launchctl unload /private/tmp/osUpdate.plist
rm -rf /private/tmp/osUpdate.plist
initializeSoftwareUpdate
pleaseWait
elif [[ $buttonClicked == 2 ]]; then
echo "User Clicked Defer"
# Set deferralCount to 0
deferralCount="3"
#Append current number of deferrals to deferlog.txt
echo $deferralCount > $deferlog
exit 0
fi
}
finalPrompt(){
buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "$finalDescription" -alignDescription "Left" -button1 "Start Now" -timeout $timeout -countdown -alignCountdown "Center")
if [[ $buttonClicked == 0 ]]; then
echo "User clicked Start Now"
echo "Initializing Software Update"
rm -rf "${logFolder}"
launchctl unload /private/tmp/osUpdate.plist
rm -rf /private/tmp/osUpdate.plist
initializeSoftwareUpdate
pleaseWait
fi
}
# First Prompt
if [[ ! -f $deferlog ]]; then
echo "Initializing deferral process"
# Create a logs folder
mkdir $logFolder
# Create a deferlog.txt to track deferral count
touch "$deferlog"
# Set deferralCount to 0
deferralCount="1"
#Append current number of deferrals to deferlog.txt
echo $deferralCount > $deferlog
# Initialize First Prompt
firstPrompt
# Second Prompt
elif [[ ${lastDeferralCount} = "1" ]]; then
echo "Initializing Second Prompt"
secondPrompt
# Third Prompt
elif [[ ${lastDeferralCount} = "2" ]]; then
echo "Initializing Third Prompt"
thirdPrompt
elif [[ ${lastDeferralCount} = "3" ]]; then
echo "Initializing Final Prompt"
finalPrompt
fi
exit 0 ## Success
exit 1 ## Failure
Posted on 07-24-2022 07:48 PM
Hi @bwoods I have encountered an issue and I'm hoping you might be able to help me out.
When I let the launchdaemon script from policy A runs after the specified time it will show the Jamf helper prompt, and when click "Start Now" nothing seems to happen except the script removing the log folder from the /Users. Also, no logs are showing up in deferral policy B logs that the user has selected "Start Now". However, If I select "Defer" it works and I can see the logs showing up in policy B that the user has deferred.
The interesting thing is if I manually run sudo jamf policy -event updaeOS on the test machine the "Start Now" from the jamf helper works fine!
Any advice would be appreciated greatly.
07-26-2022 07:53 AM - edited 07-26-2022 07:54 AM
@moussabl , ensure that the custom triggers are exactly the same. In your post above, see "updaeOS" instead of "updateOS". The trigger in the launch daemon must mach the trigger in the policy. Also, I designed the script to self destruct after "Start Now" is selected. The logs only work if the user clicks defer.
For testing I suggest using Code Runner to run Part A over and over again until you're satisfied with the results.
For instance, have the code for Part A in code Runner and Part B in Jamf. Manually set the timeout in Part A to something reasonable like 10 seconds to test the code.
Posted on 07-31-2022 06:45 PM
Thanks heaps @bwoods! It's working for us now.
Sorry, final question. would you know if there's a way to postponed the Jamf helper notification when a user is active in a meeting on MS Teams or Zoom call?
Thank you very much.
Posted on 07-31-2022 09:38 PM
@moussabl If the notification is a system notification, teach the users to enable Do not disturb on the computer at the beginning of a meeting (and how to disable it afterwards). This process is slightly different depending on the macOS version:
Posted on 08-02-2022 06:28 AM
Hmmm...my first instinct would be to create a conditional statement to check if Zoom or Teams is open, but I don't know how to tell if the user is on a call. Simply checking for open apps may result in the process never starting because messaging apps are usually always open.
05-31-2022 06:50 AM - edited 05-31-2022 06:51 AM
@MPL , Below is a screenshot of my parameters for the second part of my jamf helper process. You'll want to increase the timeout for production. I use 30 seconds for testing.
05-31-2022 08:48 AM - edited 05-31-2022 09:25 AM
@bwoods Thank you for all this! So both of these are attached to different policies?
Also, does this work with Intel devices too?
05-31-2022 10:37 AM - edited 05-31-2022 10:50 AM
You'll need two policies for this to work. Part A needs a policy with recurring check-in trigger to place the daemon. Part B needs a policy with a custom trigger. This also works on intel.
05-31-2022 10:44 AM - edited 05-31-2022 10:45 AM
Thanks! I split it up into two policies a bit ago exactly the way you said above.
It's been about an hour since the script ran and the software update began "processing" and it hasn't moved since then. It did prompt me for a restart (I exited the window) but no forced restart as of yet.
I'll wait a bit longer to see if anything happens.
In addition, do you know if this script works fine with Intel devices? Or only with M1's?
Posted on 05-31-2022 10:56 AM
Yeah, It can take a long time. It's basically all up to APNS, but you can check the computer record to see if the management action is pending. Search your computer>Management Tab>You should see something like scheduleOSUpdate or availableOSUdate. It works on Intel machines too.
Posted on 05-31-2022 10:59 AM
Just checked the computer>Management Tab and didn't see any pending/failed commands :(
Posted on 05-31-2022 11:01 AM
It'll only show as Pending if the command hasn't been sent yet. Since you're running this on the machine, it likely won't have Jamf's typically 10 minute hold before the command is sent. You should check computer > History > Management History instead.
Also note that if you haven't already run the "Download and Notify" MDM command it will likely be in the process of downloading the updates before it reboots for installing them.
Posted on 05-31-2022 11:36 AM
Gotcha. I checked the Computer > History > Management History area and do see both "ScheduleOSUpdate" and "AvailableOSUpdates" completed about an hour and a half ago. The "Software Update" window on the machine itself is still present saying it's processing the software update.
I believe this machine already had the updates downloaded to it and just needed to be restarted to complete everything prior to this policy being implemented/running.
Posted on 05-31-2022 11:54 AM
I don't believe it takes updates that have already been downloaded into account. It just downloads them again and install from there...but yeah it can take quite a while.
06-01-2022 06:39 AM - edited 06-01-2022 06:52 AM
Update - So I let the system sit overnight and it never actually restarted by itself.
In the top left it says, "Restarting Your Computer. Your computer needs to restart to install updates." and gives me a button to click to restart but it never forcefully restarted.
- Any idea why that might be?
Another thing, when I tested the deferral by clicking "Defer" the update window popped up immediately after saying I have 2 deferrals left. I tried to setup deferrals in the policy itself in JAMF but they didn't seem to sync together. Do you know why this is?
- Edit: Might be because I have the policy set to "Ongoing" hehe. My mistake!
Posted on 06-01-2022 09:02 AM
A tad confused on how to setup the deferrals actually.
Do i need to setup deferrals in the actual policy itself too? If not, should the policy be set to ongoing or? Or just select "Defer" on the window that pops up from the script and it'll take care of the rest?
Apologies - not too familiar with scripting and whatnot.
06-01-2022 09:18 AM - edited 06-01-2022 09:20 AM
I've hardcoded the script for 3 deferrals after user interaction. You can change the deferInterval in Part A with parameter $4. You can change the time out in part B with parameter $8. You can't edit much else, this is specifically what my org requested. You'd have to tinker with the script in Part B to change the description etc...
Below I have the daemon calling the script every 30 seconds. You most likely need to extend this interval.
Posted on 06-03-2022 06:58 AM
Thanks for all your help and your amazing script!
We got it working on our end for now!
Posted on 06-01-2022 04:48 AM
@bwoods I see that this script specifically calls out a macOS version to update to. Does this script also install non-macOS point release updates (ex: Command Line Tools, Xcode etc...)?
Posted on 06-01-2022 05:16 AM
@stutz , I haven't tested that yet.
Posted on 06-03-2022 08:04 AM
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.
Posted on 06-05-2022 09:12 PM
@eargueta, please rephrase your question. I don't understand the issue you're having.
Posted on 06-14-2022 05:17 PM
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.
Posted on 06-17-2022 08:57 AM
Looks like you need to choose to unload or unload your daemon and/or agent.
Posted on 06-17-2022 09:00 AM
Ah, I see that that's a mistake in the script. I'll add the unload above.
Posted on 06-17-2022 09:04 AM
@dvasquez I fixed it. Copy the script again in my previous post.
Posted on 06-23-2022 09:08 AM
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!
Posted on 06-23-2022 09:50 AM
@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.
Posted on 06-23-2022 12:07 PM
I will look for that but in early testing, I did not see any progress there.
I will test again.
Posted on 06-17-2022 11:28 AM
@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
Posted on 06-21-2022 12:19 PM
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.
Posted on 06-22-2022 05:05 AM
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?
Posted on 06-23-2022 09:53 AM
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.
Posted on 06-23-2022 08:38 AM
@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.
Posted on 06-23-2022 08:51 AM
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:
Posted on 06-23-2022 09:49 AM
@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?
Posted on 06-24-2022 08:14 AM
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.