As of macOS 10.12.4 and later, this workflow has changed slightly. Please visit the GitHub repo for an updated version of this workflow.
So you’ve spent the last few months testing and verifying that all your applications will work with macOS Sierra. Now it’s time to finally roll out the latest Mac operating system to your end users. Great! Except you quickly realize the upgrade process isn’t quite working the same way as it did for the previous version. If you use Jamf, you’re familiar with the traditional drag-and-drop of the installer application into Casper Admin or using the InstallESD.dmg. But, for some reason when you try these methods now, FileVault authenticated reboots no longer work and your users are being prompted to walk through the install process. This is definitely not the ideal experience you were hoping for.
As of macOS Sierra, Apple began enforcing the use of the startosinstall binary included in the installer application when triggering an automatic upgrade. With that, in-place upgrades now require a new train of thought. So how does IT accomplish this and get back to what they are accustomed to?
Don’t worry, we’re here to help. To start, let’s look at the documentation around the startosinstall binary.
startosinstall --usage Usage: startosinstall --applicationpath --volume Arguments --volume, a path to the target volume. --applicationpath, a path to copy of the OS installer application to start the install with. --license, prints the user license agreement only. --agreetolicense, agree to license the license you printed with --license. --rebootdelay, how long to delay the reboot at the end of preparing. This delay is in seconds and has a maximum of 300 (5 minutes). --pidtosignal, Specify a PID to which to send SIGUSR1 upon completion of the prepare phase. To bypass "rebootdelay" send SIGUSR1 back to startosinstall. --usage, prints this message. Example: startosinstall --volume /Volumes/Untitled --applicationpath “/Users/Shared/Install macOS.app"
This gives us a lot of good information to start with, so lets start building our command out…
startosinstall --volume /--applicationpath "/Users/Shared/Install macOS Sierra.app” --nointeraction
Notice the “—nointeraction” switch isn’t listed in the documentation for startosinstall. That is OK. It’s actually a hidden switch that suppresses all user interaction, including the “—agreetolicense” switch.
OK, great! We got our command, now we need to test this. If you haven’t noticed already, this command requires that the computer we are running it on must have the “Install macOS Sierra.app” on the client. So let’s open the Composer.app and package up the installer. In this example, I’m going to go ahead and stage it in /Users/Shared/.
Sweet, you can now upload that package to Jamf Pro. To do this, create a quick policy and get it staged on your test machine.
At this point, we’re ready to test our command on a machine. Start by building a script to deploy the command via Self Service.
#!/bin/bash /Users/Shared/Install\ macOS\ Sierra.app/Contents/Resources/startosinstall --volume / --applicationpath /Users/Shared/Install\ macOS\ Sierra.app --nointeraction
Cool, so we have our script now, so let’s go run it! Not so fast.
You are probably seeing for yourself that this doesn't quite work.
So what’s happening? Well, the startosinstall binary initiates a normal or soft reboot when everything is completed in order to give the user time to save any documents they may have open or close any apps they were using. Sounds like a great idea, but it doesn't quite work together with Self Service since the script hasn't completed yet and it ends up stomping on itself in the process. So, let’s work on that. We can quickly modify this simple script so that it initiates the startosinstall binary and then moves it to a background process, allowing the script to complete successfully and the reboot happen cleanly.
#!/bin/bash /Users/Shared/Install\ macOS\ Sierra.app/Contents/Resources/startosinstall --volume / --applicationpath /Users/Shared/Install\ macOS\ Sierra.app --nointeraction &
Now the script completes successfully! But, you're probably wondering why nothing is happening. Well, if you wait 5-10 minutes, the computer will reboot once the installer has completed preparing the system. So, it does work, but it’s not a great end user experience. Let’s fix this by making it more appealing to users utilizing Jamf Helper.
If you are not familiar with Jamf Helper, it is a handy tool you can use to display a basic GUI to the end user all from a bash script. Here is a quick look at the different options that are available with Jamf Helper.
jamfHelper -help Usage: jamfHelper -windowType [-windowPostion] [-title] [-heading] [-description] [-icon] [-button1] [-button2] [-defaultButton] [-cancelButton] [-showDelayOptions] [-alignDescription] [-alignHeading] [-alignCountdown] [-timeout] [-countdown] [-iconSize] [-lockHUD] [-startLaunchd] [-fullScreenIcon] [-kill] -windowType [hud | utility | fs] hud: creates an Apple "Heads Up Display" style window utility: creates an Apple "Utility" style window fs: creates a full screen window the restricts all user input WARNING: Remote access must be used to unlock machines in this mode -windowPosition [ul | ll | ur | lr] Positions window in the upper right, upper left, lower right or lower left of the user's screen If no input is given, the window defaults to the center of the screen -title "string" Sets the window's title to the specified string -heading "string" Sets the heading of the window to the specified string -description "string" Sets the main contents of the window to the specified string -icon path Sets the windows image filed to the image located at the specified path -button1 "string" Creates a button with the specified label -button2 "string" Creates a second button with the specified label -defaultButton [1 | 2] Sets the default button of the window to the specified button. The Default Button will respond to "return" -cancelButton [1 | 2] Sets the cancel button of the window to the specified button. The Cancel Button will respond to "escape" -showDelayOptions "int, int, int,..." Enables the "Delay Options Mode". The window will display a dropdown with the values passed through the string -alignDescription [right | left | center | justified | natural] Aligns the description to the specified alignment -alignHeading [right | left | center | justified | natural] Aligns the heading to the specified alignment -alignCountdown [right | left | center | justified | natural] Aligns the countdown to the specified alignment -timeout int Causes the window to timeout after the specified amount of seconds Note: The timeout will cause the default button, button 1 or button 2 to be selected (in that order) -countdown Displays a string notifying the user when the window will time out -iconSize pixels Changes the image frame to the specified pixel size -lockHUD Removes the ability to exit the HUD by selecting the close button -startlaunchd Starts the JAMF Helper as a launchd process -kill Kills the JAMF Helper when it has been started with launchd -fullScreenIcon Scales the "icon" to the full size of the window Note: Only available in full screen mode Return Values: The JAMF Helper will print the following return values to stdout… 0 - Button 1 was clicked 1 - The Jamf Helper was unable to launch 2 - Button 2 was clicked 3 - Process was started as a launchd task XX1 - Button 1 was clicked with a value of XX seconds selected in the drop-down XX2 - Button 2 was clicked with a value of XX seconds selected in the drop-down 239 - The exit button was clicked 240 - The "ProductVersion" in sw_vers did not return 10.5.X, 10.6.X or 10.7.X 243 - The window timed-out with no buttons on the screen 250 - Bad "-windowType" 254 - Cancel button was select with delay option present 255 - No "-windowType"
Lots of information there, but since it’s already on the system, we can go ahead and use these to let the end user know what’s going on. Now, besides just adding the Jamf Helper window, we can go ahead and utilize the —pidtosignal switch to close Jamf Helper cleanly so we don't have any possible conflicts during reboot.
#!/bin/bash ##Heading to be used for jamfHelper heading="Please wait as we prepare your computer for macOS Sierra…" ##Title to be used for jamfHelper description=" This process will take approximately 5-10 minutes. Once completed your computer will reboot and begin the upgrade." ##Icon to be used for jamfHelper icon=/Users/Shared/Install\ macOS\ Sierra.app/Contents/Resources/InstallAssistant.icns ##Launch jamfHelper /Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -windowType fs -title "" -icon "$icon" -heading "$heading" -description "$description" & jamfHelperPID=$(echo $!) ##Start macOS Upgrade /Users/Shared/Install\ macOS\ Sierra.app/Contents/Resources/startosinstall --volume / --applicationpath /Users/Shared/Install\ macOS\ Sierra.app --nointeraction --pidtosignal $jamfHelperPID & exit 0
Now we have Jamf Helper launching a full screen window to let the user know we are preparing their system for upgrading to macOS Sierra. Once the startosinstall binary is done doing its thing, it quietly closes Jamf Helper and lets the computer reboot cleanly — all the things we want! You'll also notice now that since we are adhering to Apple's guidelines for conducting an upgrade, the FileVault Authenticated reboot worked beautifully and loaded directly into the macOS installer volume and began the upgrade process without asking the user for anything.
Last but not least, don’t forget to clean up your Self Service descriptions and make it informative to your end users.
Now you have a basic macOS upgrade script that should help you get the project rolling and wrapped up. But, if you're looking for a little more, maybe some checks to ensure the Macs are plugged into power and have enough free space for the upgrade, head over to my GitHub page for a fully working and tested workflow.