Major OSX Update to Mojave with Time Limits and Postpone capabiliity

ssc
New Contributor II

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.

  1. Smart group for devices which ARE NOT eligible to be updated to Mojave. Please see the screenshot. Essentially the upgrade to Mojave requires the computer to have 12.5GB of storage space. However, if the installer isn’t present on the computer it does require the device to have an additional 7GB of space for the actual installer. Hence the numbers in the screenshot
  2. A policy which contains the packaged OSX Installer. I simply downloaded the latest Mojave installer to my computer, then packaged it using Composer. I placed it as a PKG under my Mojave Cache Policy and gave it a custom trigger (the name is important when running the script). In my example the custom trigger is cachemojave. Make sure you haven’t set this policy to run at any time other than via the custom trigger. Scope is open to EVERYONE in my case.
  3. A 2nd policy which contains the script and all of its parameters. This needs to have a scope that is set to your target computers (but make sure you exclude the “Not eligible for Mojave update” smart group that you made previously). Set this to run at a recurring check-in and have it as available offline. This is important if you are only running an JAMF internally.

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

b9d7407459dc43c6a1e1f66f4c669f3b

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.

a1ef9aeaec414e67b74952e0a7cafb77
dad2afee628e410cafbff2b49ecfbaee

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.

8d1b904a161a46f09ce29a5998a00e91
d3993b454d1947e6a5dfac155d43d58c

6. Here are some screenshots of what it should look like

6c425d6fd4484bc1956989289463bdc0
7dc82490a6514c0f8a6786f88dacaf82
d33e9a021048480282119e2413c2f625

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 🙂

1 REPLY 1

user-ZOLBeLujmO
New Contributor

Wow this is awesome, thanks for sharing! Was looking for some sort of better experience than the old smash smash.