Posted on 06-20-2019 08:06 PM
Hi everyone,
I thought I'd share a script that I've been using to update our students’ laptops to the next major OS version (at this time it's Mojave). This particular script is based on upgrading any OS (tested on Yosemite, El-Capitan, Sierra, High Sierra) to Mojave. This is not intended to be used for incremental updates e.g. 10.14.1 --> 10.14.5
The script contains a number of parameters which allow it to achieve the following:
• Perform updates ONLY during specified hours. This is important as we don’t want staff or students being interrupted during school hours.
• Only run if the device has enough battery or is connected to AC
• The user can post pone a maximum of 3 times, after that they have to continue with the installation
• It uses the startosinstall tool, so it performs the updates using Apple’s built in method.
• The script will download the OSX installer from your local JSS server to save internet usage
• The policy will run even when users are off site. Useful for those environments where JAMF is not externally accessible
It does require the following policies and smart groups to be configured.
There are a couple of parameters I suggest changing to work within your environment. These include the following:
• The hours in-between which you want the script to run e.g. between 7pm and 11pm
• The minimum battery percentage the computer can be to run the update
• The maximum number of times that the user is able to postpone the update
• The message which is displayed to the end users
• The JSS local server address
The following variables are very important to get right before running the updater:
The installer location
Variable Name: $INSTALLERLOCATION
Location: within the script. This must match the custom trigger of your OSX download policy
Custom trigger name
Variable Name: $CUSTOMINSTALLERTRIGGER
Location: within the script
The version build number of the installer
Variable Name: versionBuildNumberOfInstaller
Location: as a parameter within the policy
Note: this is important so that the computer has the CURRENT OSX installer placed on the computer. If the build number of the installer on the machine does not match the one set in the parameter it will remove the existing installer and re-download the latest one.
To find the build number you need to run the following: defaults read “/Applications/Install macOS Mojave.app/Contents/version.plist" CFBundleShortVersionString on a device which has the current OSX installer. Preferably the one in which you packaged the installer on.
Please read through the script comments to make sure that you understand how it works and what additional variables may need updating. This has been tested in our environment, however make sure you TEST TEST TEST before deploying this to users.
#!/bin/bash
################ Read In Variables ##########################
currentOSupdate=$4 # e.g "10.14.5 - this is the OS of the installer package that has been downloaded into the Applications folder"
startTime=$5 # e.g. Start time of policy. Needs to be in 24 hour time without trailing 00's. ex. 18. The policy will only run the installer between the startTime and endTime
endTime=$6 # e.g. End time of policy. Needs to be in 24 hour time without trailing 00's. ex. 6
batteryMinimumPercentage=$7 # e.g. 50 - do not put in percentage sign. If the battery is equal to or greater than this amount then the installer will continue
skipBatteryandACCheck=$8 # If this is equal to 1, then no battery or AC check will be performed. Not recommended, but can be used for testing
versionBuildNumberOfInstaller=$9 # The version build number is found running the following command: defaults read "<<Path to Mac OS Installer>>/Contents/version.plist" CFBundleShortVersionString
################## Set Variables ############################
# This will need to be updated depending the on the OS you are going to install
INSTALLERLOCATION="/Applications/Install macOS Mojave.app"
# You will need to configure another policy which downloads the OSX Installer. It will need a custom trigger which needs to match the one in this script
CUSTOMINSTALLERTRIGGER="cachemojave"
# The maximum number of times the installer can be post poned before the user is forced to update their OS
MAXPOSTPONE=3
# More so to avoid timeout errors if the user isn't in front of the computer when the dialog box appears
APPLEDIALOGTIMEOUT=300 # In seconds
# Set the address of your JSS Server
JSS_SERVER_ADDRESS="<<your local jss server address>>"
### IMPORTANT ####
# Check the runInstaller function to set the correct variables down there.
# Check the userPrompt command to check variables there
# I recommend not changing the following variables
LOGGEDINUSER=$(ls -l /dev/console | cut -d " " -f 4) # Find the username of the logged in user
CURRENTTIME=$((10#$(date +%H)))
OSVERSION=$(sw_vers -productVersion)
BATTERYLEVEL=$(pmset -g batt | grep -Eo "d+%" | cut -d% -f1)
AC_POWER=$(ioreg -l | grep ExternalConnected | cut -d"=" -f2 | sed -e 's/ //g')
POSTPONEREMAININGFILE="/usr/local/updateCount/postponecount"
CURRENTDATE=$(date '+%d')
log_location="/var/log/upgradeProcess.log"
#######
# This message is displayed to the end user in the Apple Dialog box. It appears when they have not reached their maximum number of postpones.
REMAININGPOSTPONEMESSAGE="A Major OSX Upgrade requires installation.
We recommend performing a Time Machine backup, or transferring important work to Google Drive before continuing.
You can postpone this installation a maximum of $MAXPOSTPONE times.
Select begin update when you are ready. It is a good idea to close any open applications before beginning the update as it REQUIRES YOUR DEVICE TO RESTART
The installation process should take roughly 30 minutes.
Regards,
<<Your business name>>."
# This message will be displayed to the end user after they have postponed the installer the maximum number of times.
NOMOREPOSTPONEMESSAGE="A Major OSX Upgrade requires installation.
You have postponed the installation a maximum number of times. You must install the update
Please be aware that the process REQUIRES YOUR DEVICE TO RESTART. The install should take 30 minutes.
Save any work before continuing.
Regards,
<<You business name>>."
#############################################################
echo "The following variables have been set"
echo "##################################################"
echo "OS which is going to be installed: ${currentOSupdate}"
echo "Laptops current OS: $OSVERSION"
echo "Policy will run between the following hours ${startTime}00 and ${endTime}00 - current time is $(date +%H)00"
echo "AC Connected: $AC_POWER"
echo "Battery Level: $BATTERYLEVEL% "
echo "##################################################"
# Define Functions #
ScriptLogging(){
DATE=`date +%Y-%m-%d %H:%M:%S`
LOG="$log_location"
echo "$DATE" " $1" >> $LOG
}
createPostPoneFile(){
if [ ! -e "$POSTPONEREMAININGFILE" ] ; then
mkdir -p "/usr/local/updateCount"
echo "No postpone count file - creating"
ScriptLogging "No postpone count file - creating"
echo "$MAXPOSTPONE" > "$POSTPONEREMAININGFILE"
touch -A "-240000" "$POSTPONEREMAININGFILE"
fi
POSTPONEFILEMODDATE=$(stat -f "%Sm" -t "%d" $POSTPONEREMAININGFILE)
}
checkPostPoneDate() {
if [ $CURRENTDATE -eq $POSTPONEFILEMODDATE ] ; then
echo "Update has been postponed already"
exit 0
fi
}
checkInstallerCached() {
if [ -e "$INSTALLERLOCATION" ]; then
echo "Installer Present - Checking OS Installer Version"
# The installer build number is important so that this script can check whether the OSX installer that is on the device is the correct version e.g. 10.14.5 not 10.14.4
INSTALLERBUILDNUMBER=$(defaults read "$INSTALLERLOCATION/Contents/version.plist" CFBundleShortVersionString)
if [[ "$versionBuildNumberOfInstaller" = "$INSTALLERBUILDNUMBER" ]]; then
echo "Installer is the correct build number - continuing"
else
echo "Installer is not the correct build number - removing installer and redownloading"
rm -rf "$INSTALLERLOCATION"
checkInstallerCached
fi
else
echo "Installer Not Found - Cacheing Installer"
if host -W .5 $JSS_SERVER_ADDRESS > /dev/null ; then
echo "On site network - downloading installer"
if jamf policy -trigger $CUSTOMINSTALLERTRIGGER | grep "No policies were found" ; then
echo "Cached installer could not be found"
exit 1
fi
else
echo "Away from site network - exiting"
exit 0
fi
fi
}
checkOSVERSION() {
if [ $currentOSupdate = $OSVERSION ]; then
echo "Computers OS is up to date - removing the installer if it exists on the computer"
rm -rf "$INSTALLERLOCATION"
rm -f $POSTPONEREMAININGFILE
if host -W .5 $JSS_SERVER_ADDRESS > /dev/null ; then
echo "Device on site. Performing recon"
jamf recon
fi
exit 0
fi
}
checkTime() {
DOW=$(date +%u) #Find day of week e.g. Saturday = 6, Sunday = 7
# If it is a Sat or Sun then run this
if [[ $DOW =~ ^[6-7]+$ ]]; then
echo "Date of week falls in the correct range"
else
if (( $startTime > $endTime )); then
if (( $CURRENTTIME >= $startTime || $CURRENTTIME < $endTime )); then
echo "Script falls within time limits - continuing"
else
echo "Script exiting as time does not fall within limits"
exit 0
fi
else
if (( $startTime <= $CURRENTTIME && $CURRENTTIME < $endTime )); then
echo "Script falls within time limits - continuing"
else
echo "Script exiting as time does not fall within limits"
exit 0
fi
fi
fi
}
checkBattery() {
if [ "$AC_POWER" == "Yes" ]; then
echo "AC Power Is Connected - Continuing"
elif [ "$AC_POWER" == "No" ] && [ $BATTERYLEVEL -gt $batteryMinimumPercentage ]; then
echo "AC Power Is Not Connected - sufficient battery to continue"
else
echo "Insufficient battery power - exiting"
exit 0
fi
}
runInstaller() {
echo "User has opted to update - installer will now run. Resetting updateCountFile"
rm $POSTPONEREMAININGFILE
#Heading to be used for jamfHelper
heading="Update to OSX Mojave Operating System"
#Title to be used for jamfHelper
description="The update to Mojave is initializing in the background. This process will take approximately 5-10 minutes.
Now is a good time to save any documents you have open and close all windows.
Do not close your laptop, it will restart automatically to finalise the updates.
Please visit ICT if you have any issues"
#Icon to be used for jamfHelper. This will need to be updated if the OSX installer is not Mojave
icon=/Applications/Install macOS Mojave.app/Contents/Resources/InstallAssistant.icns
#Launch jamfHelper
/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -windowType utility -title "" -icon "$icon" -heading "$heading" -description "$description" &
if "${INSTALLERLOCATION}"/Contents/Resources/startosinstall --volume / --agreetolicense --nointeraction ;
then
sleep 1200
killall jamfHelper
killall jamf
else
echo "Installation failed due to insufficient space - will remove installer and run recon"
killall jamfHelper
jamf recon
rm -rf "$INSTALLERLOCATION"
exit 1
fi
}
findRemainingPostPones() {
postPoneRemaining=$(cat $POSTPONEREMAININGFILE)
if [[ $postPoneRemaining -le 0 ]]; then
updateMessage=$NOMOREPOSTPONEMESSAGE
else
updateMessage=$REMAININGPOSTPONEMESSAGE
fi
userPrompt
}
userPrompt() {
## Important
## To make sure that the correct icon is displayed in the dialog box you will need to update the line where it says "application file id "com.apple.InstallAssistant.Mojave"
## It needs to reflect the OS installer that has been placed on the computer. E.g. for High Sierra it would have been com.apple.InstallAssistant.HighSierra.
## It may require trial and error to get this right
updateResult=$(
su - $LOGGEDINUSER -c osascript <<EOD
tell application "Finder"
set InstallerPath to (application file id "com.apple.InstallAssistant.Mojave" as alias) & "Contents:Resources:InstallAssistant.icns" as string
end tell
set dialogResult to display dialog "$updateMessage" buttons {$(if [ $postPoneRemaining -gt 0 ]; then echo ""Postpone (${postPoneRemaining} Remaining )","; fi) "Begin Update"} default button 1 with icon file InstallerPath with title "<<Your business name>>" giving up after $APPLEDIALOGTIMEOUT
if the button returned of the result is "Begin Update" then
set updateStatus to "Update"
else if gave up of dialogResult then
set updateStatus to "TimeOut"
else
set updateStatus to "PostPone"
end if
return updateStatus
EOD
)
if [ "$updateResult" == "PostPone" ] ; then
echo "Decreasing postpone count"
((postPoneRemaining--))
echo $postPoneRemaining > $POSTPONEREMAININGFILE
echo $postPoneRemaining
ScriptLogging "User has POSTPONED. Number remaining is: $postPoneRemaining"
elif [ "$updateResult" == "Update" ] ; then
ScriptLogging "User has hit UPDATE - PRE runInstaller Command"
runInstaller
elif [ "$updateResult" == "TimeOut" ] ; then
echo "Timeout has occured - exiting"
exit 0
fi
}
############################################
# Start Script #
############################################
checkOSVERSION
checkInstallerCached
createPostPoneFile
checkPostPoneDate
checkTime
if [[ "$skipBatteryandACCheck" == "1" ]]; then
echo "Skipping battery and AC check"
else
checkBattery
fi
findRemainingPostPones
Here's how I would go about setting it up:
1. Create your smartgroup for computers which are not eligible for the update
2. Package your OSX installer using composer. This should place the installer in the applications folder
3. Create the policy to download the OSX installer to the device.
4. Upload the script and modify it to suit your environment. There are details above about how to do this. Read through the comments carefully so you understand how it works. Set the parameter labels under the script options if you like - that way they appear in the policy.
5. Create the policy for the main prompt and set the script parameters here. Make sure your scope is correct here.
6. Here are some screenshots of what it should look like
Please bear in mind this is my first post to JAMF nation. Let me know if you have any questions about this script or where I can improve it or help you out :)
Posted on 04-06-2021 05:51 AM
Wow this is awesome, thanks for sharing! Was looking for some sort of better experience than the old smash smash.