#!/bin/zsh
#
# Utility - OS Upgrade - Deadline.sh
#
#
# Enforces a macOS upgrade. Requires a Jamf policy to cache an "Install macOS" app on the user's Mac.
# Before starting, checks:
# * Current OS version does not match the target upgrade version already.
#
# Before installing, checks:
# * If user is running on a MacBook. If so: if they are running on battery or not.
# * If the installer app exists.
# * If there is enough free space in GB after caching the installer app.
# * If no user is logged in: start upgrade immediately.
#
# Creates a first-boot script and launch daemon to run the script.
# The script runs after a successful upgrade to clean up the installer app and immediately try to submit a Jamf recon.
# Jamf recon may not succeed in all cases if network is not up at the time the recon tries to run.
#
# Script logs: /Library/Logs/AG/
# OS upgrade log: /var/log/startosinstall.log
# Exit Codes:
# 0: Success
# 1: Generic error
# 2: OS already up to date
# 3: Failed to cache OS installer
# 4: Not enough free space
# 5: Running on MacBook without AC power
########################################
# Global Variables
########################################
osUpgradeName="${4}" # e.g. 'macOS 12 Monterey'
osUpgradeMajorVersion="${5}" # For macOS 11 and newer just use the major digit like '11' ; For older use the first two, like '10.15'
osUpgradeAppPath="${6}" # e.g. '/Applications/Install macOS Monterey.app'
requiredInstallSpace=${7} # Space needed for installation of the upgrade in GB
jamfPolicyCacheInstaller="${8}" # Jamf policy trigger to cache the desired macOS installer
notificationName="AG-Utility-OSUpgrade-Deadline" # Name of notification - for logging purposes
titleTxt="${osUpgradeName} Upgrade" # Notification window title and heading text
iconPath="/Library/Application Support/AGCustom/XXXXXXX_XXXX_Vertical_Wordmark.ico" # Icon shown in notification window
loggedInUser=$( /bin/echo "show State:/Users/ConsoleUser" | /usr/sbin/scutil | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }' ) # Get the current user - shell agnostic (sh, bash, zsh, dash)
myJamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" # Short variable for using jamfHelper
caffeinatePID="" # Set up for caffeinate later
freeSpace=$(/bin/df -g / | /usr/bin/awk 'FNR==2{print $4}') # Free space in GB
originalOsVer=$(/usr/bin/sw_vers -productVersion) # OS version for comparing to in post-upgrade script - used to determine if cleanup should be done
finishOSInstallScriptFilePath="/usr/local/jamfps/finishOSInstall.sh" # Post-upgrade script to remove the installers and recon
osinstallersetupdDaemonSettingsFilePath="/Library/LaunchDaemons/com.jamfps.cleanupOSInstall.plist" # Post-upgrade launch daemon to start script above
osinstallLogfile="/var/log/startosinstall.log" # Output from macOS installer
########################################
########################################
# Logging
########################################
logPath="/Library/Logs/AG"
logFile="${logPath}/${notificationName}.log"
if [ ! -d "$logPath" ]; then
/bin/mkdir -p "$logPath"
fi
WriteLog() {
logTxt=$1
/bin/echo "$(/bin/date +%Y-%m-%d_%H:%M:%S) : ${logTxt}" | /usr/bin/tee -a "$logFile"
}
########################################
########################################
# Support Functions
########################################
# Kill a process by PID if it is found
KillProcess() {
processPID="$1"
if /bin/ps -p "$processPID" > /dev/null ; then
/bin/kill "$processPID"
wait "$processPID" 2>/dev/null
fi
}
# Clean up processes and files and exit with the given exit code
CleanExit() {
WriteLog "INFO: Running clean exit with exit code ($1)."
# Kill caffeinate process
if [ -n "$caffeinatePID" ]; then
WriteLog "INFO: Killing caffeinate process ($caffeinatePID)."
KillProcess "$caffeinatePID"
fi
# Remove first time login daemon and script
if [ -f "$finishOSInstallScriptFilePath" ]; then
WriteLog "INFO: Deleting ($finishOSInstallScriptFilePath)."
/bin/rm -f "$finishOSInstallScriptFilePath"
fi
if [ -f "$osinstallersetupdDaemonSettingsFilePath" ]; then
WriteLog "INFO: Deleting ($osinstallersetupdDaemonSettingsFilePath)."
/bin/rm -f "$osinstallersetupdDaemonSettingsFilePath"
fi
exit "$1"
}
# If previous processes remain for some reason, the installation will freeze, so kill those proccesses.
ProcessCleanup() {
killingProcesses=("caffeinate" "startosinstall" "osinstallersetupd")
for processName in "${killingProcesses[@]}"; do
[ -z "$processName" ] && continue
WriteLog "INFO: Killing ($processName) processes."
/usr/bin/killall "$processName" 2>&1 || true
done
}
# Used to show an error message to the user appended with standard information.
# Parameter should be a string of text you want to show the user.
ShowError()
{
if [ "$loggedInUser" != "" ] && [ "$loggedInUser" != "_mbsetupuser" ] && [ "$loggedInUser" != "root" ] && [ "$loggedInUser" != "loginwindow" ]; then
errorTxt=$1
/usr/bin/osascript -e "display dialog \"$errorTxt
The ${osUpgradeName} upgrade script has failed. Please submit a ticket to your XXXX OIT Service Area for assistance at
https://help.xxxx.xxxxxxx.edu
The XXXX OIT ticket submission form will open in your default browser after you exit this dialog.\" buttons {\"Exit\"} default button \"Exit\" with title \"Error: Upgrade Script Failed\" with icon POSIX file \"$iconPath\""
/usr/bin/sudo -u "$loggedInUser" /usr/bin/open
fi
}
### Create First Boot Script ###
# Creates a first boot script under /usr/local/jamfps
# Script cleans up macOS installer app if an upgrade succeeded and also runs a recon
# Deletes itself and the launch daemon that calls it
# Because the parent shell script creates a new shell script using HEREDOC ( this << EOF >that ),
# use a backslash before the dollar sign to avoid evaluating the variable (or command) as part of creating the script.
# Note: we WANT to evaluate some variables from this parent script (e.g. the daemon settings path)
CreateFirstBootScript() {
WriteLog "INFO: Creating first boot script to run a Jamf recon and delete installer app on successful upgrade."
/bin/mkdir -p /usr/local/jamfps
/bin/cat << EOF > "$finishOSInstallScriptFilePath"
#!/bin/zsh
## First Run Script to remove the installer.
## Wait until /var/db/.AppleUpgrade disappears
while [ -e /var/db/.AppleUpgrade ];
do
echo "\$(date "+%a %h %d %H:%M:%S"): Waiting for /var/db/.AppleUpgrade to disappear." >> /usr/local/jamfps/firstbootupgrade.log
/bin/sleep 60
done
## Wait until the upgrade process completes
INSTALLER_PROGRESS_PROCESS=\$(/usr/bin/pgrep -l "Installer Progress")
until [ "\$INSTALLER_PROGRESS_PROCESS" = "" ];
do
echo "\$(date "+%a %h %d %H:%M:%S"): Waiting for Installer Progress to complete." >> /usr/local/jamfps/firstbootupgrade.log
/bin/sleep 1
INSTALLER_PROGRESS_PROCESS=\$(/usr/bin/pgrep -l "Installer Progress")
done
## Clean up installer app and recon if we upgraded
osVer=\$(/usr/bin/sw_vers -productVersion)
if [ "\$osVer" != "$originalOsVer" ]; then
if [ -e "$osUpgradeAppPath" ]; then
/bin/rm -rf "$osUpgradeAppPath"
fi
/usr/local/jamf/bin/jamf recon
fi
## Remove LaunchDaemon
if [ -e "$osinstallersetupdDaemonSettingsFilePath" ]; then
/bin/rm -f "$osinstallersetupdDaemonSettingsFilePath"
fi
## Remove Script
if [ -e "$finishOSInstallScriptFilePath" ]; then
/bin/rm -f "$finishOSInstallScriptFilePath"
fi
exit 0
EOF
/usr/sbin/chown root:admin "$finishOSInstallScriptFilePath"
/bin/chmod 755 "$finishOSInstallScriptFilePath"
}
### Create Launch Daemon ###
# Create launch daemon to kick off our first boot script above
CreateLaunchDaemon() {
/bin/cat << EOF > "$osinstallersetupdDaemonSettingsFilePath"
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.jamfps.cleanupOSInstall</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-c</string>
<string>$finishOSInstallScriptFilePath</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
EOF
# Set the permission on the file just made.
/usr/sbin/chown root:wheel "$osinstallersetupdDaemonSettingsFilePath"
/bin/chmod 644 "$osinstallersetupdDaemonSettingsFilePath"
}
########################################
########################################
# Pre-check
########################################
# If we're already on our target OS version - stop
# Check if the full detected OS version starts with our desired major OS version number
if echo "$originalOsVer" | /usr/bin/grep -q "^$osUpgradeMajorVersion"; then
WriteLog "WARN: Upgrade target is macOS ${osUpgradeMajorVersion}. Mac is running macOS ${originalOsVer}. Stopping."
CleanExit 2
fi
########################################
########################################
# Main Script
########################################
# Reset ignored software updates
WriteLog "INFO: Resetting ignored software updates."
/usr/sbin/softwareupdate --reset-ignored