├── .gitattributes
├── .gitignore
├── Extension Attributes
├── currentdeferral-ea.sh
├── lastdefercount-ea.sh
└── lastupdate-ea.sh
├── Jamf Pro Scripts
├── cached pkg installer.sh
├── cached pkg processor.sh
├── download macos installer.sh
└── self service macos upgrade-or-erase.sh
├── LICENSE
├── Package
├── ROOT
│ ├── Applications
│ │ └── Utilities
│ │ │ └── README.txt
│ └── Library
│ │ └── LaunchDaemons
│ │ └── com.corp.patcher-every2h.plist
└── scripts
│ ├── postinstall.sh
│ └── preinstall.sh
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/Extension Attributes/currentdeferral-ea.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # EA to show how many deferrals are currently used
4 |
5 | # Reads from the deferral file on the mac if it exists
6 | # We return an output as an integer only for proper audit and processing later.
7 |
8 | test=$( /usr/bin/plutil -extract deferral raw -o - /usr/local/corp/cachedapps/appupdates.plist )
9 |
10 | if [ ! -z "$test" ];
11 | then
12 | echo "$test"
13 | else
14 | echo "0"
15 | fi
16 |
--------------------------------------------------------------------------------
/Extension Attributes/lastdefercount-ea.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # EA to show how many deferrals were used at last update
4 |
5 | # Reads from the deferral file on the mac if it exists
6 | # We return an output as an integer only for proper audit and processing later.
7 |
8 | test=$( /usr/bin/plutil -extract lastdeferral raw -o - /usr/local/corp/cachedapps/appupdates.plist )
9 |
10 | if [ ! -z "$test" ];
11 | then
12 | echo "$test"
13 | else
14 | echo "0"
15 | fi
16 |
--------------------------------------------------------------------------------
/Extension Attributes/lastupdate-ea.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # EA to show the date of last update
4 |
5 | # Reads from the deferral file on the mac if it exists
6 | # We return an output as a Jamf compliant date YYYY-MM-DD hh:mm:ss
7 |
8 | test=$( /usr/bin/plutil -extract lastupdate raw -o - /usr/local/corp/cachedapps/appupdates.plist | /usr/bin/tr 'T' ' ' | /usr/bin/tr -d "Z" )
9 |
10 | if [ ! -z "$test" ];
11 | then
12 | echo "$test"
13 | else
14 | echo "1970-01-01 09:00:00"
15 | fi
16 |
--------------------------------------------------------------------------------
/Jamf Pro Scripts/cached pkg installer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Main patching and installer script
4 | # Meant to be run periodically from launchd on macOS endpoint.
5 | # Now with silent loginwindow support.
6 | # richard@richard-purves.com - 17-03-2023 - v2.3
7 |
8 | # Logging output to a file for testing
9 | #set -x
10 | #logfile=/Users/Shared/cachedappinstaller.log
11 | #exec > $logfile 2>&1
12 |
13 | # Set user display messages here
14 | msgtitlenewsoft="New Software Available"
15 | msgprogresstitle="Updating Software"
16 | msgosupgtitle="Upgrading macOS"
17 | msgnewsoftware="Important software updates are available!
18 |
19 | The following new software is ready for upgrade:"
20 | msgnewsoftforced="Important software updates are available!
21 |
22 | You have run out of allowed install deferrals.
23 |
24 | The following software will be upgraded now:"
25 | msgrebootwarning="Your computer now needs to restart to complete the updates."
26 | msgpowerwarning="Your computer is about to upgrade installed software.
27 |
28 | Please ensure you are connected to AC Power.
29 |
30 | Once you are plugged in, please click the Proceed button."
31 | msgosupgradewarning="Your computer will perform a major macOS upgrade.
32 |
33 | Please ensure you are connected to AC Power.
34 |
35 | Your computer will restart and the OS upgrade process will continue. It will take up to 90 minutes to complete.
36 |
37 | IT IS VERY IMPORTANT YOU DO NOT INTERRUPT THIS PROCESS."
38 |
39 | # Script variables here
40 | alloweddeferral="5"
41 | forcedupdate="0"
42 | blockingapps=( "Microsoft PowerPoint" "Keynote" "zoom.us" )
43 | silent="0"
44 | waitroom="/Library/Application Support/JAMF/Waiting Room"
45 | workfolder="/usr/local/corp"
46 | infofolder="$workfolder/cachedapps"
47 | imgfolder="$workfolder/imgs"
48 | updatefilename="appupdates.plist"
49 | updatefile="$infofolder/$updatefilename"
50 | pbjson="/private/tmp/progressbar.json"
51 | canceljson="/private/tmp/progresscancel.json"
52 | installoutput="/private/tmp/installout.log"
53 | stosout="/private/tmp/upgrade.log"
54 | jsspkginfo="/private/tmp/jsspkginfo.tsv"
55 |
56 | jssurl=$( /usr/bin/defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url )
57 | jb=$( which jamf )
58 | osa="/usr/bin/osascript"
59 | pbapp="/Applications/Utilities/Progress.app"
60 | pb="$pbapp/Contents/MacOS/Progress"
61 | installiconpath="/System/Library/CoreServices/Installer.app/Contents/Resources"
62 | updateicon="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns"
63 | currentuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' )
64 |
65 | if [ ! -z "$currentuser" ];
66 | then
67 | curuserid=$( id -u "$currentuser" 2>/dev/null )
68 | else
69 | curuserid="501"
70 | fi
71 |
72 | homefolder=$( dscl . -read /Users/$currentuser NFSHomeDirectory | awk '{ print $2 }' )
73 | bootvolname=$( /usr/sbin/diskutil info / | /usr/bin/awk '/Volume Name:/ { print substr($0, index($0,$3)) ; }' )
74 |
75 | # Check that the info folder exists, create if missing and set appropriate permissions
76 | /bin/mkdir -p "$infofolder"
77 | /bin/chmod 755 "$infofolder"
78 | /usr/sbin/chown root:wheel "$infofolder"
79 |
80 | ####################
81 | # #
82 | # Let's get to it! #
83 | # #
84 | ####################
85 |
86 | # Keep the mac awake while this runs.
87 | /usr/bin/caffeinate -dis &
88 |
89 | # Stop IFS splitting on spaces
90 | OIFS=$IFS
91 | IFS=$'\n'
92 |
93 | # Is anyone logged in? Engage silent mode.
94 | [ "$currentuser" = "loginwindow" ] || [ -z "$currentuser" ] && silent="1"
95 | echo "Current user: $currentuser"
96 |
97 | # Is the screen locked? Quit if idle time under eight hours.
98 | if [ "$(/usr/libexec/PlistBuddy -c "print :IOConsoleUsers:0:CGSSessionScreenIsLocked" /dev/stdin 2>/dev/null <<< "$(/usr/sbin/ioreg -n Root -d1 -a)")" = "true" ];
99 | then
100 | idletime=$( /usr/sbin/ioreg -c IOHIDSystem | /usr/bin/awk '/HIDIdleTime/ {print int($NF/1000000000); exit}')
101 | echo "Screen Locked. User idle time: $idletime"
102 | [ "$idletime" -lt 14400 ] && { echo "Idle time less than four hours. Exiting."; exit 0; }
103 | else
104 | echo "Screen Unlocked."
105 | fi
106 |
107 | # Blocking application check here. Find the foreground app with lsappinfo assuming silent mode isn't engaged
108 | if [ "$silent" = "0" ];
109 | then
110 | foregroundapp=$( /usr/bin/lsappinfo list | /usr/bin/grep -B 4 "(in front)" | /usr/bin/awk -F '\\) "|" ASN' 'NF > 1 { print $2 }' )
111 |
112 | # check for blocking apps
113 | for app ($blockingapps)
114 | do
115 | [ "$app" = "$foregroundapp" ] && { echo "Blocking app: $app"; exit 0; }
116 | done
117 | fi
118 |
119 | ########################################
120 | #
121 | # Find and process any cached installers
122 | #
123 | ########################################
124 |
125 | # Do we have a temp file that still exists? Clear it if so to avoid duplicate entries
126 | [ -f "$jsspkginfo" ] && /bin/rm "$jsspkginfo"
127 |
128 | # Find all previously cached files
129 | cachedpkg=($( find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) ))
130 |
131 | # Did we find any? Quit if not.
132 | [ ${#cachedpkg[@]} = 0 ] && { echo "No cached files found. Exiting."; exit 0; }
133 |
134 | # Process the array of files into a tsv for later sorting
135 | for pkgfilename ($cachedpkg)
136 | do
137 | # Now read out all the info we've collected from the cache file.
138 | priority=$( /usr/bin/plutil -extract Priority raw -o - "${pkgfilename}" )
139 | pkgname=$( /usr/bin/plutil -extract PkgName raw -o - "${pkgfilename}" )
140 | displayname=$( /usr/bin/plutil -extract DisplayName raw -o - "${pkgfilename}" )
141 | fullpath=$( /usr/bin/plutil -extract FullPath raw -o - "${pkgfilename}" )
142 | reboot=$( /usr/bin/plutil -extract Reboot raw -o - "${pkgfilename}" )
143 | feu=$( /usr/bin/plutil -extract FEU raw -o - "${pkgfilename}" )
144 | fut=$( /usr/bin/plutil -extract FUT raw -o - "${pkgfilename}" )
145 | osinstall=$( /usr/bin/plutil -extract OSInstaller raw -o - "${pkgfilename}" )
146 |
147 | # Check for any spurious no name filenames. Skip if found.
148 | [ "$pkgfilename" = ".plist" ] && continue
149 |
150 | # Check to see if any of the critical fields are blank. Skip if so.
151 | [ -z "$pkgname" ] || [ -z "$fullpath" ] && continue
152 |
153 | # Check to see if we've a match in the cached folder. Skip if not.
154 | # We check for both file and directory in case of dmg, flat pkg or non flat pkg.
155 | [ ! -f "$fullpath/$pkgname" ] && [ ! -d "$fullpath/$pkgname" ] && continue
156 |
157 | # Store everything into a tsv temporary file
158 | echo -e "${priority}\t${pkgname}\t${displayname}\t${fullpath}\t${reboot}\t${feu}\t${fut}\t${osinstall}" >> "$jsspkginfo"
159 |
160 | # Clean up of variables before next loop or end of loop
161 | unset priority pkgname displayname fullpath reboot feu fut osinstall
162 | done
163 |
164 | # Did we even write out a tsv file
165 | if [ ! -f "$jsspkginfo" ];
166 | then
167 | # Output fail message, then clean up files and folders
168 | echo "No processed tsv file detected. Aborting."
169 | /usr/bin/find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) -exec rm {} \;
170 | /usr/bin/find "$waitroom" \( -iname \*.pkg -o -iname \*.cache.xml \) -exec rm {} \;
171 | exit 0
172 | fi
173 |
174 | # Sort the file using priority number, then alphabetical order on filename
175 | /usr/bin/sort "$jsspkginfo" -o "$jsspkginfo"
176 |
177 | # Check to see if there's a macOS installer
178 | # placed a space between the -d and the $ due to suspected Jamf PI being logged.
179 | osinstall=$( /usr/bin/tail -n 1 "$jsspkginfo" | /usr/bin/cut -f2 -d $'\t' )
180 | [[ "$osinstall" == *"Install macOS"* ]] && osinstall="1" || osinstall="0"
181 |
182 | ################################
183 | #
184 | # Prompt the user about updating
185 | #
186 | ################################
187 |
188 | # Do we have a defer file. Initialize one if not.
189 | [ ! -f "$updatefile" ] && /usr/bin/defaults write "$updatefile" deferral -int 0
190 |
191 | # Read deferral count
192 | deferred=$( /usr/bin/defaults read "$updatefile" deferral )
193 |
194 | # Work out icon path for osascript. It likes paths in the old : separated format
195 | [ -f "$installiconpath/Installer.icns" ] && icon="$installiconpath/Installer.icns"
196 | [ -f "$installiconpath/AppIcon.icns" ] && icon="$installiconpath/AppIcon.icns"
197 | iconposix=$( echo $icon | /usr/bin/sed 's/\//:/g' )
198 | iconposix="$bootvolname$iconposix"
199 |
200 | # Prepare list of apps to install in readable format
201 | # Remove trailing blank lines to avoid parsing issues
202 | while read line || [ -n "$line" ];
203 | do
204 | applist+=$( echo $line | cut -f2 -d$'\t' )"\\n"
205 | done < "$jsspkginfo"
206 |
207 | applist=$( echo $applist | awk /./ )
208 |
209 | # Check deferral count. Prompt user if under, otherwise force the issue.
210 | # If silent mode then skip all this and just do it
211 | if [ "$silent" = "0" ];
212 | then
213 | if [ "$deferred" -lt "$alloweddeferral" ];
214 | then
215 | # Prompt user that updates are ready. Allow deferral.
216 | test=$( /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "'"$msgnewsoftware"'\n\n'"$applist"'\n\nAuto deferral in 60 seconds." giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Install", "Defer"} default button 2' )
217 |
218 | # Did we defer?
219 | if [ $( echo $test | /usr/bin/grep -c -e "Defer" -e "gave up:true" ) = "1" ];
220 | then
221 | # Increment counter and store.
222 | deferred=$(( deferred + 1 ))
223 | /usr/bin/defaults write "$updatefile" deferral -int "$deferred"
224 |
225 | # Notify user how many deferrals are left and exit.
226 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "You have used '"$deferred"' of '"$alloweddeferral"' allowed upgrade deferrals." giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Ok"} default button 1'
227 | exit 0
228 | fi
229 | else
230 | # Prompt user that updates are happening right now.
231 | forced="1"
232 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "'"$msgnewsoftforced"'\n\n'"$applist"'\n\nThe upgrade will start in 60 seconds." giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Install"} default button 1'
233 | fi
234 | fi
235 |
236 | # Store deferrals used, time of last update and then reset the deferral count
237 | /usr/bin/defaults write "$updatefile" lastdeferral -int $( /usr/bin/defaults read "$updatefile" deferral )
238 | /usr/bin/defaults write "$updatefile" lastupdate -date "$( /bin/date "+%Y-%m-%d %H:%M:%S" )"
239 | /usr/bin/defaults write "$updatefile" deferral -int 0
240 |
241 | ###################################
242 | #
243 | # Check to see if we're on AC power
244 | #
245 | ###################################
246 |
247 | if [ "$silent" = "0" ];
248 | then
249 | # Check if device is on battery or ac power
250 | # Valid reports are `Battery Power` or `AC Power`
251 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 )
252 |
253 | # Warn the user if not on AC power
254 | count=1
255 | while [ "$count" -le "3" ];
256 | do
257 | [ "$pwrAdapter" = "AC Power" ] && break
258 | count=$(( count + 1 ))
259 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "'"$msgpowerwarning"'" giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Proceed"} default button 1'
260 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 )
261 | done
262 |
263 | ################################
264 | #
265 | # Close all running applications
266 | #
267 | ################################
268 |
269 | # Less severe way of closing only the applications we need to
270 | # Iterate through previously captured tsv file and strip off versioning
271 | # Now strip off any trailing whitespace to stop things failing
272 | # Finally work out PID numbers from a wildcard search of what's left and ...
273 | # kill -9 them all.
274 | while read line || [ -n "$line" ];
275 | do
276 | app=$( echo $line | /usr/bin/cut -f3 -d$'\t' | /usr/bin/sed "s/[-0-9.]*$//" )
277 | [ "${app: -1}" = " " ] && app=${app[1,-2]}
278 | pids=$( /bin/ps ax | /usr/bin/grep -i "$app" | /usr/bin/grep -v grep | /usr/bin/awk '{ print $1 }' )
279 | [ ! -z "$pids" ] && echo $pids | /usr/bin/xargs kill -9
280 | done
281 | fi
282 |
283 | ######################################
284 | #
285 | # Start installing from the build list
286 | #
287 | ######################################
288 |
289 | # Store total number of applications to install
290 | # Displayed to user later during progress bar
291 | appcounter=0
292 | totalappnumber=${#cachedpkg[@]}
293 |
294 | # Invoke our progress bar application.
295 | # Set initial message and then run the app as a background app.
296 | cat < "$pbjson"
297 | {
298 | "percentage": -1,
299 | "title": "$msgprogresstitle",
300 | "message": "Preparing to upgrade ...",
301 | "icon": "$updateicon"
302 | }
303 | EOF
304 |
305 | [ "$silent" = "0" ] && $pb $pbjson $canceljson &
306 |
307 | # Read the tsv line by line and install
308 | while read line
309 | do
310 | # Increment the app counter
311 | appcounter=$(( appcounter + 1 ))
312 |
313 | # Batch process the line we just read out into its component parts
314 | pkgname=$( echo "$line" | cut -f2 )
315 | displayname=$( echo "$line" | cut -f3 )
316 | fullpath=$( echo "$line" | cut -f4 )
317 | reboot=$( echo "$line" | cut -f5 )
318 | feu=$( echo "$line" | cut -f6 )
319 | fut=$( echo "$line" | cut -f7 )
320 | osinstall=$( echo "$line" | cut -f8 )
321 |
322 | # Does this or any other installer require a restart.
323 | # Mark it so with an empty file.
324 | [ "$reboot" = "1" ] && touch /private/tmp/.apppatchreboot
325 |
326 | # Have the Jamf FEU/FUT options been set for this package
327 | [ "$feu" = "1" ] && installoption="$installoption -feu"
328 | [ "$fut" = "1" ] && installoption="$installoption -fut"
329 |
330 | # Is this an OS install. Break out of the loop. Handle this separately.
331 | [ "$osinstall" = "1" ] && continue
332 |
333 | # Perform the installation as a background task with the correct options
334 | # Output progress to a text file. We'll use that next.
335 | /usr/sbin/installer -target / -package "${fullpath}/${pkgname}" -verboseR > "$installoutput" &
336 |
337 | while :;
338 | do
339 | # Wait three seconds for Progress to update, then work out current percentage.
340 | # We make sure the percentage never hits 100 or the progress bar will stop.
341 | sleep 1
342 |
343 | # Did someone try to hit the cancel button? Kill the generated file and restart the progress bar.
344 | # They had a chance to defer earlier.
345 | if [ "$forced" = "0" ];
346 | then
347 | if [ -f "$canceljson" ];
348 | then
349 | /bin/rm "$canceljson"
350 | /usr/bin/killall Progress 2>/dev/null
351 | /bin/rm /private/tmp/.apppatchreboot
352 | break
353 | fi
354 | else
355 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; }
356 | fi
357 |
358 | percent=$( /bin/cat "$installoutput" | /usr/bin/grep "installer:%" | /usr/bin/cut -d"%" -f2 | /usr/bin/awk '{ print int($1) }' | /usr/bin/tail -n1 )
359 | [ "$percent" -eq 100 ] && percent="99"
360 |
361 | # Correct the file that Progress is using to display the bar.
362 | cat < "$pbjson"
363 | {
364 | "percentage": $percent,
365 | "title": "Installing $displayname",
366 | "message": "Install $percent% completed - (Updating $appcounter of $totalappnumber)",
367 | "icon": "$updateicon"
368 | }
369 | EOF
370 |
371 | # Check to see if we've had a finished install. Break out the loop if so.
372 | complete=$( /bin/cat "$installoutput" | /usr/bin/grep -c -E "successful|failed" )
373 | [ "$complete" != "0" ] && { /bin/rm -f "$installoutput"; break; }
374 | done
375 |
376 | # Clean up of variables before next loop or end of loop
377 | unset priority pkgname displayname fullpath reboot feu fut
378 |
379 | done < "$jsspkginfo"
380 |
381 | # Finally end the progress bar
382 | if [ "$osinstall" = "0" ];
383 | then
384 | cat < "$pbjson"
385 | {
386 | "percentage": 99,
387 | "title": "$msgprogresstitle",
388 | "message": "Application Updates Completed",
389 | "icon": "$updateicon"
390 | }
391 | EOF
392 | fi
393 |
394 | # Kill Progress and warn the user if any impending reboots, if not in silent mode
395 | if [ "$silent" = "0" ];
396 | then
397 | [ -f /private/tmp/.apppatchreboot ] && "$osa" -e 'display dialog "'"$msgrebootwarning"'" giving up after 15 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Ok"} default button 1'
398 | fi
399 |
400 | ############################
401 | #
402 | # OS Upgrade code goes here
403 | #
404 | ############################
405 |
406 | if [ "$osinstall" = "1" ];
407 | then
408 | echo "Update macOS initiated"
409 | # Work out where startosinstaller binary is located
410 | # Most of this work should be done already from the cached file info.
411 | startos=$( /usr/bin/find "$fullpath/$pkgname" -iname "startosinstall" -type f )
412 |
413 | # Set up a future interminate progress bar here. We'll invoke this after.
414 | cat < "$pbjson"
415 | {
416 | "percentage": -1,
417 | "title": "$msgosupgtitle",
418 | "message": "Preparing to upgrade ..",
419 | "icon": "$updateicon"
420 | }
421 | EOF
422 |
423 | # Attempt to suppress certain update dialogs
424 | /usr/bin/touch "$homefolder"/.skipbuddy
425 |
426 | # Create a post install script and launchd for use after the OS upgrade.
427 | # This is why we don't need to run a recon immediately after the script runs. We do it later.
428 |
429 | cat << "EOF" > /usr/local/corp/finishOSInstall.sh
430 | #!/bin/zsh
431 |
432 | # First Run Script after an OS upgrade.
433 |
434 | # Wait until /var/db/.AppleUpgrade disappears
435 | while [ -e /var/db/.AppleUpgrade ]; do sleep 5; done
436 |
437 | # Wait until the upgrade process completes
438 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" )
439 | until [ "$INSTALLER_PROGRESS_PROCESS" = "" ];
440 | do
441 | sleep 15
442 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" )
443 | done
444 |
445 | # Look for a user
446 | loggedinuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' )
447 |
448 | # If loginwindow, setup assistant or no user, then we're in a DEP environment.
449 | if [ "$loggedinuser" = "loginwindow" ] || [ "$loggedinuser" = "_mbsetupuser" ] || [ "$loggedinuser" = "root" ] || [ -z "$loggedinuser" ];
450 | then
451 | # Now check to see if Setup Assistant is a running process. Exit if so.
452 | [ $( /usr/bin/pgrep "Setup Assistant" ) ] && exit 0
453 |
454 | # We're fine to restart loginwindow at this point.
455 | /usr/bin/killall -9 loginwindow
456 | fi
457 |
458 | # Update Device Information
459 | /usr/local/bin/jamf manage
460 | /usr/local/bin/jamf recon
461 | /usr/local/bin/jamf policy
462 |
463 | # Remove LaunchDaemon
464 | /bin/rm -f /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
465 |
466 | # Remove Script
467 | /bin/rm -f /usr/local/corp/finishOSInstall.sh
468 |
469 | exit 0
470 | EOF
471 |
472 | /usr/sbin/chown root:admin /usr/local/corp/finishOSInstall.sh
473 | /bin/chmod 755 /usr/local/corp/finishOSInstall.sh
474 |
475 | # Create a LaunchDaemon to run the above script
476 | cat << "EOF" > /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
477 |
478 |
479 |
480 |
481 | Label
482 | com.corp.cleanupOSInstall
483 | ProgramArguments
484 |
485 | /bin/zsh
486 | -c
487 | /usr/local/corp/finishOSInstall.sh
488 |
489 | RunAtLoad
490 |
491 |
492 |
493 | EOF
494 |
495 | /usr/sbin/chown root:wheel /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
496 | /bin/chmod 644 /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
497 |
498 | # Are we running on Intel or Arm based macs? Apple Silicon macs require user credentials.
499 | if [ $( /usr/bin/arch ) = "arm64" ];
500 | then
501 | # Apple Silicon macs. We need to prompt for the users credentials or this won't work. Skip this totally if in silent mode.
502 | if [ "$silent" = "0" ];
503 | then
504 |
505 | # Work out appropriate icon for use
506 | icon="/System/Applications/Utilities/Keychain Access.app/Contents/Resources/AppIcon.icns"
507 | iconposix=$( echo $icon | /usr/bin/sed 's/\//:/g' )
508 | iconposix="$bootvolname$iconposix"
509 |
510 | # Warn user of what's about to happen
511 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "We about to upgrade your macOS and need you to authenticate to continue.\n\nPlease enter your password on the next screen.\n\nPlease contact IT Helpdesk with any issues." giving up after 60 with icon file "'"$iconposix"'" with title "macOS Upgrade" buttons {"OK"} default button 1'
512 |
513 | # Loop three times for password validation
514 | count=1
515 | while [ "$count" -le 3 ];
516 | do
517 |
518 | # Prompt for a password. Verify it works a maximum of three times before quitting out.
519 | # Also have timeout on the prompt so it doesn't just sit there.
520 | password=$( /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "Please enter your macOS login password:" default answer "" with title "macOS Update - Authentication Required" giving up after 300 with text buttons {"OK"} default button 1 with hidden answer with icon file "'"$iconposix"'" ' -e 'return text returned of result' '' )
521 |
522 | # Escape any spaces in the password
523 | escapepassword=$( echo ${password} | /usr/bin/sed 's/ /\\\ /g' )
524 |
525 | # Ok verify the input we got is correct
526 | validpassword=$( /usr/bin/expect </dev/null
560 | /usr/bin/killall caffeinate 2>/dev/null
561 | /bin/rm -f /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
562 | /bin/rm -f "$workfolder"/finishOSInstall.sh
563 | /usr/bin/find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) -exec rm {} \;
564 | /usr/bin/find "$waitroom" \( -iname \*.pkg -o -iname \*.cache.xml \) -exec rm {} \;
565 | exit 1
566 | fi
567 |
568 | # Temporarily disable Jamf Connect Login
569 | /usr/local/bin/authchanger -reset
570 |
571 | # Invoke startosinstall to perform the OS upgrade with the accepted credential. Run that in background.
572 | # Then start up the progress bar app.
573 | "$startos" --agreetolicense --forcequitapps --user "$currentuser" --stdinpass <<< "$password" &> "$stosout" &
574 | fi
575 | else
576 | # Intel macs. We can just go for it.
577 |
578 | # Temporarily disable Jamf Connect Login
579 | /usr/local/bin/authchanger -reset
580 |
581 | # Invoke startosinstall to perform the OS upgrade. Run that in background.
582 | # Then start up the progress bar app.
583 | "$startos" --agreetolicense --forcequitapps &> "$stosout" &
584 | fi
585 |
586 | # Code to allow people to cancel the update window. Also will not proceed until startosinstall is complete.
587 | while :;
588 | do
589 | sleep 1
590 |
591 | # Stops the user cancelling the progress bar by force reloading it if we're not forcing things.
592 | if [ "$forced" = "0" ];
593 | then
594 | if [ -f "$canceljson" ];
595 | then
596 | /bin/rm "$canceljson"
597 | /usr/bin/killall startosinstall 2>/dev/null
598 | break 3
599 | fi
600 | else
601 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; }
602 | fi
603 |
604 | # This was such a pain to work out. We have to cat the entire file out,
605 | # then use grep to find the particular line we want but this is also a trap.
606 | # Apple's startosinstall is using ^M characters to backspace and overwrite the percentage line in Terminal.
607 | # Unfortunately nohup is capturing all those characters and not doing that. We must then clean up.
608 | # So change all those to unix linefeeds with tr, grab the latest (last) line and ...
609 | # finally awk to convert to a suitable integer for use with Progress.
610 | percent=$( /bin/cat "$stosout" | /usr/bin/grep "Preparing: " | /usr/bin/tr '\r' '\n' | /usr/bin/tail -n1 | /usr/bin/awk '{ print int($2) }' )
611 | waittest=$( /bin/cat "$stosout" | /usr/bin/grep -c "Waiting to restart" | /usr/bin/tr '\r' '\n' )
612 |
613 | # Trap edge cases of numbers being 0, which won't display or 100 which stops the progress bar.
614 | [ "$percent" -eq 0 ] && percent="1"
615 | [ "$percent" -ge 99 ] && percent="99"
616 |
617 | cat < "$pbjson"
618 | {
619 | "percentage": $percent,
620 | "title": "$msgosupgtitle",
621 | "message": "macOS upgrade $percent% completed. Please wait.",
622 | "icon": "$updateicon"
623 | }
624 | EOF
625 | # If we detected the restart message, break out the loop here.
626 | [ "$waittest" = "1" ] && break
627 |
628 | # If startosinstall quit suddenly, break here too
629 | [ -z $( /usr/bin/pgrep startosinstall ) ] && { error=1; break; }
630 | done
631 | fi
632 |
633 | # Did startosinstall quit part way? Warn user.
634 | if [ "$error" = "1" ];
635 | then
636 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "The upgrade encountered an unexpected error.\n\nPlease try again later." giving up after 60 with icon file "'"$iconposix"'" with title "Error" buttons {"OK"} default button 1'
637 | fi
638 |
639 | # Run a jamf recon here so we don't overdo it by having it run every policy, only on success.
640 | # Unless we're doing an OS install, we have other ways for that above.
641 | # Was a reboot requested? We should oblige IF we're not doing an OS upgrade
642 | # Give it a 1 minute delay to allow for policy reporting to finish
643 | if [ "$osinstall" = "0" ];
644 | then
645 | $jb recon
646 | if [ -f "/private/tmp/.apppatchreboot" ];
647 | then
648 | /bin/rm /private/tmp/.apppatchreboot
649 | /sbin/shutdown -r +0.1 &
650 | fi
651 | fi
652 |
653 | # Stop caffeinate so we can sleep again, then clean up files
654 | /usr/bin/killall Progress 2>/dev/null
655 | /usr/bin/killall caffeinate 2>/dev/null
656 | /bin/rm -f "$jsspkginfo"
657 |
658 | # Clean these ONLY if we didn't cancel out
659 | if [ ! -f "$canceljson" ];
660 | then
661 | /usr/bin/find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) -exec rm {} \;
662 | /usr/bin/find "$waitroom" \( -iname \*.pkg -o -iname \*.cache.xml \) -exec rm {} \;
663 | fi
664 |
665 | # Reset IFS
666 | IFS=$OIFS
667 |
668 | # All done
669 | exit 0
670 |
--------------------------------------------------------------------------------
/Jamf Pro Scripts/cached pkg processor.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Cached application pkg processor script
4 | # Meant to be run after an application installer is cached to the mac
5 | # richard@richard-purves.com - 07-02-2022 - v1.3
6 |
7 | # Variables here
8 | waitroom="/Library/Application Support/JAMF/Waiting Room"
9 | infofolder="/usr/local/corp/cachedapps"
10 | apiusr=""
11 | apipwd=""
12 | jssurl=$( /usr/bin/defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url )
13 |
14 | # Check that the info folder exists
15 | # Create if missing and set appropriate permissions
16 | /bin/mkdir -p "$infofolder"
17 | /bin/chmod 755 "$infofolder"
18 | /usr/sbin/chown root:wheel "$infofolder"
19 |
20 | # Let's get to it!
21 |
22 | # Check to see if the OS Install switch has been used
23 | osappbundle="$4"
24 |
25 | # Lastly work out todays date.
26 | # We may use this in the future, but at the moment it probably won't be.
27 | tdydate=$( /bin/date -u +"%Y-%m-%dT%H:%M:%SZ" )
28 |
29 | # Special case for the OS installer switch
30 | if [ -z "$osappbundle" ];
31 | then
32 | # We're not an OS install bundle
33 | /bin/echo "OS Installer not specified"
34 |
35 | # Find the most recent file in the Jamf Waiting Room folder
36 | # We're specifically looking for .pkg or .pkg.zip files. We don't want to deal with DMG packages and should not.
37 | # We also have to avoid any .pkg.cache.xml files as well. These contain info but it's not useful to us.
38 |
39 | # Find all the *.pkg or *.pkg.zip files in thw waiting room. Sort them by last modified date, old to new and then grab the last one.
40 | cachedpkg=$( find "$waitroom" -type f \( -iname \*.pkg -o -iname \*.pkg.zip \) -print0 |\
41 | xargs -0 ls -tr |\
42 | tail -n1 )
43 |
44 | # Did we pick up a file? Fail here if not.
45 | [[ -z "$cachedpkg" ]] && { echo "No package found."; exit 1; }
46 |
47 | # Strip off the full file path so we just have the name
48 | # Then remove the file extension from the name
49 | # Finally work out the cache folder patch
50 | pkgfilename=$( /usr/bin/basename "$cachedpkg" )
51 | displayname=${pkgfilename%.*}
52 | cachepath=$( /usr/bin/dirname "$cachedpkg" )
53 |
54 | # Use a Jamf API request to pull the pkg record from Jamf Pro.
55 | # Use xmllint's xpath to parse through the xml output to get the field we want.
56 | # Strip off the xml open and close tags, then store in a variable.
57 | pkgrecord=$( /usr/bin/curl -H "Accept: application/xml" -s -u "${apiusr}:${apipwd}" "${jssurl}JSSResource/packages/name/${pkgfilename}" -X GET )
58 | priority=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/priority' - | /usr/bin/sed -e 's/<[^>]*>//g' )
59 | reboot=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/reboot_required' - | /usr/bin/sed -e 's/<[^>]*>//g' )
60 | feu=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/fill_existing_users' - | /usr/bin/sed -e 's/<[^>]*>//g' )
61 | fut=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/fill_user_template' - | /usr/bin/sed -e 's/<[^>]*>//g' )
62 |
63 | # Now write out all the info we've collected to our cache file. We'll process these in another script.
64 | # The name of the file should be date-name.plist.
65 | # Create the file with plutil and then insert the keys required afterwards. Original defaults code was throwing weird errors from macOS 12.
66 | /usr/bin/plutil -create xml1 "$infofolder"/"${pkgfilename}".plist
67 | /usr/bin/plutil -insert PkgName -string "$pkgfilename" "$infofolder"/"${pkgfilename}".plist
68 | /usr/bin/plutil -insert FullPath -string "$cachepath" "$infofolder"/"${pkgfilename}".plist
69 | /usr/bin/plutil -insert DisplayName -string "$displayname" "$infofolder"/"${pkgfilename}".plist
70 | /usr/bin/plutil -insert Priority -integer "$priority" "$infofolder"/"${pkgfilename}".plist
71 | /usr/bin/plutil -insert Reboot -bool "$reboot" "$infofolder"/"${pkgfilename}".plist
72 | /usr/bin/plutil -insert CacheDate -date "$tdydate" "$infofolder"/"${pkgfilename}".plist
73 | /usr/bin/plutil -insert FEU -bool "$feu" "$infofolder"/"${pkgfilename}".plist
74 | /usr/bin/plutil -insert FUT -bool "$fut" "$infofolder"/"${pkgfilename}".plist
75 | /usr/bin/plutil -insert OSInstaller -bool FALSE "$infofolder"/"${pkgfilename}".plist
76 |
77 | # Look for any already cached pkgs and cache files. We already have a processed name from earlier.
78 | # Count number of dash characters in filename, then count number of spaces
79 | dashes=$( echo "$displayname" | awk -F\- '{print NF-1}' )
80 | spaces=$( echo "$displayname" | awk -F\ '{print NF-1}' )
81 |
82 | # Slice the name after the delimiter.
83 | # If we have dashes, process those otherwise move to spaces.
84 | # Thanks to many things here's the insanity that we have:
85 | # echo out the name of the package. Reverse it so it's backwards.
86 | # split the reversed name and tell cut command to include everything forward.
87 | # reverse everything again to get the correct output.
88 | # I wish there was something better than this but limited on our cli tools.
89 | if [ "$dashes" -gt "0" ];
90 | then
91 | name=$( echo $displayname | rev | cut -d"-" -f2- | rev )
92 | elif [ "$spaces" -gt "0" ];
93 | then
94 | name=$( echo $pkgname | rev | cut -d" " -f2- | rev )
95 | else
96 | echo "ERROR: Can't split filename. Can't detect duplicate names."
97 | exit 1
98 | fi
99 |
100 | # Remove any duplicates using some zsh search to exclude most recent modified date files.
101 | # Direct output from the rm commands to null because if there's no duplicates, it'll error.
102 | /bin/rm ${waitroom}/${name}*.pkg(.om[2,-1]) 2>&1 > /dev/null
103 | /bin/rm ${waitroom}/${name}*.pkg.zip(.om[2,-1]) 2>&1 > /dev/null
104 | /bin/rm ${waitroom}/${name}*.pkg.cache.xml(.om[2,-1]) 2>&1 > /dev/null
105 | /bin/rm ${infofolder}/*${name}*.plist(.om[2,-1]) 2>&1 > /dev/null
106 | else
107 | # We are an OS installer. Special rules apply
108 | /bin/echo "OS Installer specified"
109 |
110 | # First find the app installer
111 | app=$( /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 )
112 |
113 | # Did we pick up an installer?
114 | if [ ! -z "$app" ];
115 | then
116 | # We did
117 | echo "Installer found: $app"
118 |
119 | # Work out it's name and path
120 | appname=$( /usr/bin/basename $app )
121 | apppath=$( /usr/bin/dirname $app )
122 |
123 | # Write out a mostly hard coded file for later processing. Make sure OSInstaller is set for later use.
124 | /usr/bin/plutil -create xml1 "$infofolder"/"$appname".plist
125 | /usr/bin/plutil -insert PkgName -string "$appname" "$infofolder"/"$appname".plist
126 | /usr/bin/plutil -insert FullPath -string "$apppath" "$infofolder"/"$appname".plist
127 | /usr/bin/plutil -insert DisplayName -string "$appname" "$infofolder"/"$appname".plist
128 | /usr/bin/plutil -insert Priority -integer 20 "$infofolder"/"$appname".plist
129 | /usr/bin/plutil -insert Reboot -bool FALSE "$infofolder"/"$appname".plist
130 | /usr/bin/plutil -insert CacheDate -date "$tdydate" "$infofolder"/"$appname".plist
131 | /usr/bin/plutil -insert FEU -bool FALSE "$infofolder"/"$appname".plist
132 | /usr/bin/plutil -insert FUT -bool FALSE "$infofolder"/"$appname".plist
133 | /usr/bin/plutil -insert OSInstaller -bool TRUE "$infofolder"/"$appname".plist
134 | else
135 | echo "Installer not found: $app"
136 | fi
137 | fi
138 |
139 | # All done. Package is ready for next cache install cycle.
140 | exit 0
141 |
--------------------------------------------------------------------------------
/Jamf Pro Scripts/download macos installer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Script to download the latest macOS installer
4 | # richard@richard-purves.com - 12-29-2021 - v1.7
5 |
6 | # Logging output to a file for testing
7 | #time=$( date "+%d%m%y-%H%M" )
8 | #set -x
9 | #logfile=/Users/Shared/oscache-"$time".log
10 | #exec > $logfile 2>&1
11 |
12 | exitcode=0
13 |
14 | # Any OS specified?
15 | downloados="$4"
16 | [ -z "$downloados" ] && { /bin/echo "No OS version specified. Defaulting to 12.1"; downloados="12.1"; }
17 |
18 | # Check if we're already running
19 | if [ -f "/private/tmp/.downloadmacos" ];
20 | then
21 | # Work out last modified date on the touch file. Convert to epoch.
22 | # If it's above 7200 it's been over two hours. We can ignore.
23 | currentdate=$( /bin/date -j -f "%a %b %d %T %Z %Y" "$( /bin/date )" "+%s" )
24 | lastmodified=$( /usr/bin/stat -x /private/tmp/.downloadmacos | grep "Access: " | cut -d" " -f2- )
25 | lastmodepoch=$( /bin/date -j -f "%a %b %d %T %Y" "$lastmodified" "+%s" )
26 | diff="$((currentdate-lastmodepoch))"
27 | echo "Difference: $diff"
28 |
29 | [ "$diff" -lt 7200 ] && { echo "Download already in progress."; exit 0; }
30 | fi
31 |
32 | # Place a check file to stop this running multiple times
33 | [ ! -f "/private/tmp/.downloadmacos" ] && /usr/bin/touch /private/tmp/.downloadmacos
34 |
35 | # Find and delete any existing macOS installers
36 | /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 -exec rm -rf {} \;
37 |
38 | # A quick disk space check
39 | diskinfo=$( /usr/sbin/diskutil info -plist / )
40 | freespace=$( /usr/libexec/PlistBuddy -c "Print :APFSContainerFree" /dev/stdin <<< "$diskinfo" 2>/dev/null || /usr/libexec/PlistBuddy -c "Print :FreeSpace" /dev/stdin <<< "$diskinfo" 2>/dev/null || /usr/libexec/PlistBuddy -c "Print :AvailableSpace" /dev/stdin <<< "$diskinfo" 2>/dev/null )
41 | requiredspace=$(( 45 * 1000 ** 3 ))
42 |
43 | if [ "$freespace" -ge "$requiredspace" ];
44 | then
45 | /bin/echo "Disk Check: OK - $((freespace / 1000 ** 3)) GB Free Space Detected"
46 | else
47 | /bin/echo "Disk Check: ERROR - $((freespace / 1000 ** 3)) GB Free Space Detected."
48 | exit 1
49 | fi
50 |
51 | # Reset the softwareupdate daemon
52 | /usr/bin/defaults delete /Library/Preferences/com.apple.SoftwareUpdate.plist
53 | rm /Library/Preferences/com.apple.SoftwareUpdate.plist
54 | /bin/launchctl kickstart -k system/com.apple.softwareupdated
55 |
56 | # Now use softwareupdate to cache the latest app bundle
57 | echo "macOS 10.15 or later"
58 | echo "Downloading macOS $downloados from Software Update"
59 | ( cd /Applications ; /usr/sbin/softwareupdate --fetch-full-installer --full-installer-version "$downloados" )
60 | [ ! $? = "0" ] && exitcode=1
61 |
62 | # Now find and hide the installer as people keep deleting it
63 | app=$( /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 )
64 | [ ! -z "$app" ] && /usr/bin/chflags hidden "$app"
65 |
66 | # Clean our download flag file
67 | rm -f /private/tmp/.downloadmacos
68 |
69 | # Run a recon
70 | /usr/local/bin/jamf recon
71 |
72 | exit $exitcode
73 |
--------------------------------------------------------------------------------
/Jamf Pro Scripts/self service macos upgrade-or-erase.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Script to either upgrade or erase and reinstall macOS on a target device
4 | # richard@richard-purves.com - 09-16-2021
5 |
6 | # Relies on https://github.com/adriannier/swift-progress to provide an accurate progress bar.
7 |
8 | # Choices are "upgrade" or "erase". Users will never see "erase", that one is locked to IT only.
9 | mode="$4"
10 | [ -z "$4" ] && { /bin/echo "ERROR - No operating mode specified"; exit 1; }
11 | mode=$( echo $mode | tr '[:upper:]' '[:lower:]' )
12 |
13 | # Which OS do we want to keep
14 | osexclude="$5"
15 | [ -z "$5" ] && { /bin/echo "ERROR - No OS name specified"; exit 1; }
16 |
17 | # Any OS specified?
18 | downloados="$6"
19 | [ -z "$6" ] && { /bin/echo "No OS version specified. Defaulting to 11.4"; downloados="11.4"; }
20 |
21 | #
22 | # Set up the variables here
23 | #
24 |
25 | # Work out machine type
26 | [[ $( /usr/bin/arch ) = "arm64" ]] && asmac=true || asmac=false
27 |
28 | # Work out where helper apps exist
29 | jb=$( which jamf )
30 | osa=$( /usr/bin/which osascript )
31 | pbapp="/Applications/Utilities/Progress.app"
32 | pb="$pbapp/Contents/MacOS/Progress"
33 |
34 | # Work out current user
35 | currentuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' )
36 | userid=$( /usr/bin/id -u $currentuser )
37 |
38 | # Now what is the boot volume called, just in case someone renamed it
39 | bootvolname=$( /usr/sbin/diskutil info / | /usr/bin/awk '/Volume Name:/ { print substr($0, index($0,$3)) ; }' )
40 |
41 | # Work out update icon location
42 | updateicon="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns"
43 | iconposix=$( echo $updateicon | /usr/bin/sed 's/\//:/g' )
44 | iconposix="$bootvolname$iconposix"
45 |
46 | # File locations
47 | workfolder="/private/tmp"
48 | pbjson="$workfolder/progressbar.json"
49 | canceljson="$workfolder/cancelfile.json"
50 |
51 | # Display messages are set here
52 | [[ "$mode" = "upgrade" ]] && msgosupgtitle="Upgrading macOS" || msgosupgtitle="Erasing macOS"
53 | msgpowerwarning="Your computer is about to upgrade installed software.
54 |
55 | Please ensure you are connected to AC Power.
56 |
57 | Once you are plugged in, please click the Proceed button."
58 | msgdltitle="Downloading macOS"
59 | msgdlfailed="The macOS download failed.
60 |
61 | Please check your network connection and try again later."
62 |
63 | # Keep the mac awake while this runs.
64 | /usr/bin/caffeinate -dis &
65 |
66 | # Check for Progress.app and install if not
67 | [ ! -d "/Applications/Utilities/Progress.app" ] && $jb policy -event patchinstall
68 |
69 | #
70 | # Check to see if we're on AC power
71 | #
72 |
73 | # Valid reports are `Battery Power` or `AC Power`
74 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 )
75 |
76 | # Warn the user if not on AC power
77 | count=1
78 | while [[ "$count" -le "3" ]];
79 | do
80 | [[ "$pwrAdapter" = "AC Power" ]] && break
81 | count=$(( count + 1 ))
82 | /bin/launchctl asuser "$userid" "$osa" -e 'display dialog "'"$msgpowerwarning"'" giving up after 30 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Proceed"} default button 1'
83 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 )
84 | done
85 |
86 | #
87 | # Clear any existing installers and download the specified one
88 | #
89 |
90 | # Find all macOS installers and clear them out
91 | /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 -exec rm -rf {} \;
92 |
93 | #
94 | # Download the latest approved macOS
95 | #
96 |
97 | # Reset the softwareupdate daemon
98 | /usr/bin/defaults delete /Library/Preferences/com.apple.SoftwareUpdate.plist
99 | rm /Library/Preferences/com.apple.SoftwareUpdate.plist
100 | /bin/launchctl kickstart -k system/com.apple.softwareupdated
101 |
102 | # Prep the progress bar info
103 | cat < "$pbjson"
104 | {
105 | "percentage": -1,
106 | "title": "$msgdltitle $osexclude $downloados",
107 | "message": "Preparing to download ...",
108 | "icon": "$updateicon"
109 | }
110 | EOF
111 |
112 | # Now use softwareupdate to cache the latest app bundle
113 | ( cd /Applications ; /usr/sbin/softwareupdate --fetch-full-installer --full-installer-version "$downloados" &> /private/tmp/su.log & )
114 | $pb $pbjson $canceljson &
115 |
116 | while :;
117 | do
118 |
119 | sleep 2
120 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; }
121 |
122 | percent=$( /bin/cat /private/tmp/su.log | /usr/bin/grep "Installing: " | /usr/bin/tr '\r' '\n' | /usr/bin/tail -n1 | /usr/bin/awk '{ print int($2) }' )
123 | [ "$percent" -eq 0 ] && percent="1"
124 | [ "$percent" -ge 99 ] && percent="99"
125 |
126 | complete=$( /bin/cat /private/tmp/su.log | /usr/bin/tr '\r' '\n' | /usr/bin/grep -c "Install finished successfully" )
127 | failed=$( /bin/cat /private/tmp/su.log | /usr/bin/tr '\r' '\n' | /usr/bin/grep -c "Install failed with error" )
128 |
129 | [[ "$complete" == "1" ]] || [[ "$failed" == "1" ]] && { sleep 3; break; }
130 |
131 | cat < "$pbjson"
132 | {
133 | "percentage": $percent,
134 | "title": "$msgdltitle $osexclude $downloados",
135 | "message": "macOS $percent% downloaded. Please wait.",
136 | "icon": "$updateicon"
137 | }
138 | EOF
139 |
140 | done
141 |
142 | [[ "$complete" == "1" ]] && message="macOS Download Completed" || message="macOS Download Failed"
143 |
144 | cat < "$pbjson"
145 | {
146 | "percentage": 100,
147 | "title": "$msgdltitle $osexclude $downloados",
148 | "message": "$message",
149 | "icon": "$updateicon"
150 | }
151 | EOF
152 |
153 | sleep 2
154 | /usr/bin/killall Progress 2>/dev/null
155 |
156 | # Error out if fail at this point
157 | if [ "$failed" = "1" ];
158 | then
159 | /usr/bin/killall Progress
160 | /bin/rm -f "$pbjson"
161 | /bin/rm -f "$canceljson"
162 | /bin/rm -f /private/tmp/su.log
163 | /bin/launchctl asuser "$userid" "$osa" -e 'display dialog "'"$msgdlfailed"'" giving up after 15 with icon file "'"$iconposix"'" with title "'"$msgdltitle"'" buttons {"Ok"} default button 1'
164 | exit 0
165 | fi
166 |
167 | #
168 | # Proceed with the OS work
169 | #
170 |
171 | startos=$( /usr/bin/find "/Applications" -iname "startosinstall" -type f )
172 |
173 | [ "$mode" = "upgrade" ] && msgosupgtitle="Upgrading macOS" || msgosupgtitle="Erasing macOS to $downloados"
174 |
175 | cat < "$pbjson"
176 | {
177 | "percentage": -1,
178 | "title": "$msgosupgtitle",
179 | "message": "Preparing to $mode ..",
180 | "icon": "$updateicon"
181 | }
182 | EOF
183 |
184 | if [[ "$asmac" == "true" ]];
185 | then
186 | # Apple Silicon macs. We need to prompt for the users credentials or this won't work. Skip this totally if in silent mode.
187 |
188 | # Warn user of what's about to happen
189 | /bin/launchctl asuser "$userid" "$osa" -e 'display dialog "We about to upgrade your macOS and need you to authenticate to continue.\n\nPlease enter your password on the next screen.\n\nPlease contact IT Helpdesk with any issues." giving up after 30 with icon file "'"$iconposix"'" with title "macOS Upgrade" buttons {"OK"} default button 1'
190 |
191 | # Loop three times for password validation
192 | count=1
193 | while [[ "$count" -le 3 ]];
194 | do
195 |
196 | # Prompt for a password. Verify it works a maximum of three times before quitting out.
197 | # Also have timeout on the prompt so it doesn't just sit there.
198 | password=$( "$os" -e 'display dialog "Please enter your macOS login password:" default answer "" with title "macOS Update - Authentication Required" giving up after 300 with text buttons {"OK"} default button 1 with hidden answer with icon file "'"$iconposix"'" ' -e 'return text returned of result' '' )
199 |
200 | # Escape any spaces in the password
201 | escapepassword=$( echo ${password} | /usr/bin/sed 's/ /\\\ /g' )
202 |
203 | # Ok verify the input we got is correct
204 | validpassword=$( /usr/bin/expect </dev/null
238 | /usr/bin/killall caffeinate 2>/dev/null
239 | /bin/rm -f "$pbjson"
240 | /bin/rm -f "$canceljson"
241 | /bin/rm -f /private/tmp/su.log
242 | exit 1
243 | fi
244 | fi
245 |
246 | $pb $pbjson $canceljson &
247 |
248 | cat << "EOF" > /usr/local/corp/finishOSInstall.sh
249 | #!/bin/zsh
250 |
251 | # First Run Script after an OS upgrade.
252 |
253 | # Wait until /var/db/.AppleUpgrade disappears
254 | while [ -e /var/db/.AppleUpgrade ]; do sleep 5; done
255 |
256 | # Wait until the upgrade process completes
257 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" )
258 | until [ "$INSTALLER_PROGRESS_PROCESS" = "" ];
259 | do
260 | sleep 15
261 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" )
262 | done
263 |
264 | # Update Device Information
265 | /usr/local/bin/jamf manage
266 | /usr/local/bin/jamf recon
267 | /usr/local/bin/jamf policy
268 |
269 | # Remove LaunchDaemon
270 | /bin/rm -f /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
271 |
272 | # Remove Script
273 | /bin/rm -f /usr/local/corp/finishOSInstall.sh
274 |
275 | exit 0
276 | EOF
277 |
278 | /usr/sbin/chown root:admin /usr/local/corp/finishOSInstall.sh
279 | /bin/chmod 755 /usr/local/corp/finishOSInstall.sh
280 |
281 | cat << "EOF" > /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
282 |
283 |
284 |
285 |
286 | Label
287 | com.corp.cleanupOSInstall
288 | ProgramArguments
289 |
290 | /bin/zsh
291 | -c
292 | /usr/local/corp/finishOSInstall.sh
293 |
294 | RunAtLoad
295 |
296 |
297 |
298 | EOF
299 |
300 | /usr/sbin/chown root:wheel /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
301 | /bin/chmod 644 /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist
302 |
303 | if [ "$mode" = "upgrade" ];
304 | then
305 | # macOS upgrade mode
306 | if [[ "$asmac" == "true" ]];
307 | then
308 | # Apple Silicon Macs
309 | echo "apple silicon upgrade startosinstall"
310 | "$startos" --agreetolicense --rebootdelay 120 --forcequitapps --user "$currentuser" --stdinpass <<< "$password" &> /private/tmp/upgrade.log &
311 | else
312 | # Intel Macs
313 | echo "intel upgrade startosinstall"
314 | "$startos" --agreetolicense --rebootdelay 120 --forcequitapps &> /private/tmp/update.log &
315 | fi
316 | fi
317 |
318 | if [ "$mode" = "erase" ];
319 | then
320 | # macos erase to default mode
321 | if [[ "$asmac" == "true" ]];
322 | then
323 | # Apple Silicon Macs
324 | echo "apple silicon erase startosinstall"
325 | "$startos" --agreetolicense --eraseinstall --newvolumename "Macintosh HD" --rebootdelay 120 --forcequitapps --user "$currentuser" --stdinpass <<< "$password" &> /private/tmp/update.log &
326 | else
327 | # Intel Macs
328 | echo "intel erase startosinstall"
329 | "$startos" --agreetolicense --eraseinstall --newvolumename "Macintosh HD" --rebootdelay 120 --forcequitapps &> /private/tmp/update.log &
330 | fi
331 | fi
332 |
333 | while :;
334 | do
335 | sleep 2
336 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; }
337 |
338 | percent=$( /bin/cat /private/tmp/update.log | /usr/bin/grep "Preparing: " | /usr/bin/tr '\r' '\n' | /usr/bin/tail -n1 | /usr/bin/awk '{ print int($2) }' )
339 |
340 | [ "$percent" -eq 0 ] && percent="1"
341 | [ "$percent" -ge 99 ] && percent="99"
342 |
343 | test=$( /bin/cat /private/tmp/update.log | /usr/bin/tr '\r' '\n' | /usr/bin/grep -c "Waiting to restart" )
344 | [[ "$test" == "1" ]] && { break; }
345 |
346 | cat < "$pbjson"
347 | {
348 | "percentage": $percent,
349 | "title": "$msgosupgtitle",
350 | "message": "macOS $mode $percent% completed. Please wait.",
351 | "icon": "$updateicon"
352 | }
353 | EOF
354 | done
355 |
356 | sleep 3
357 |
358 | cat < "$pbjson"
359 | {
360 | "percentage": 100,
361 | "title": "$msgosupgtitle",
362 | "message": "macOS $mode completed. Restart IMMINENT.",
363 | "icon": "$updateicon"
364 | }
365 | EOF
366 |
367 | # Clean up all files
368 | /usr/bin/killall Progress 2>/dev/null
369 | /usr/bin/killall caffeinate 2>/dev/null
370 | /bin/rm -f "$pbjson"
371 | /bin/rm -f "$canceljson"
372 | /bin/rm -f /private/tmp/su.log
373 | /bin/rm -f /private/tmp/update.log
374 |
375 | exit
376 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Richard Purves
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package/ROOT/Applications/Utilities/README.txt:
--------------------------------------------------------------------------------
1 | Progress.app to go here
2 | https://github.com/adriannier/swift-progress
3 |
--------------------------------------------------------------------------------
/Package/ROOT/Library/LaunchDaemons/com.corp.patcher-every2h.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | com.corp.patcher-every2h
7 | ProgramArguments
8 |
9 | /usr/local/bin/jamf
10 | policy
11 | -event
12 | apppatch
13 | -randomDelaySeconds
14 | 300
15 |
16 | StartInterval
17 | 7200
18 | UserName
19 | root
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Package/scripts/postinstall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Post installation script
4 |
5 | # Work out locations of files
6 | pb=$( /usr/bin/find /Applications -type d -iname "Progress.app" -maxdepth 2 )
7 | ld=$( find /Library/LaunchDaemons -iname "*apppatcher*.plist" -type f -maxdepth 1 )
8 |
9 | # Clear any quarantine flags because they randomly set for some reason best known to Apple
10 | /usr/bin/xattr -r -d com.apple.quarantine $pb
11 | /usr/bin/xattr -r -d com.apple.quarantine $ld
12 |
13 | # Hide the progress app
14 | /usr/bin/chflags hidden "$pb"
15 |
16 | # Finally load the launchdaemon
17 | /bin/launchctl load "$ld"
18 |
19 | exit
20 |
--------------------------------------------------------------------------------
/Package/scripts/preinstall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Remove any existing patch setup preinstall script
4 |
5 | # Work out locations of files
6 | /usr/bin/find /Applications -type d -iname "Progress.app" -maxdepth 2 -exec rm -rf {} \;
7 | /usr/bin/find /Library/LaunchDaemons -iname "*apppatcher*.plist" -type f -maxdepth 1 -exec rm -f {} \;
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Mac Patcher and Upgrader
2 | ========================
3 |
4 | This is a series of scripts and a deployment pkg to plug into your Jamf Pro system for both Application and OS based upgrades.
5 |
6 | ### Overview ###
7 |
8 | * Gives a nice UI prompt to end users showing updates are available.
9 | * Deferment mechanism allowing users not to update immediately unless they run out.
10 | * Will update installed applications.
11 | * If available, will upgrade the installed OS.
12 | * Accurate progress bar information such as:
13 | * * Install 23% completed - (Updating 2 of 5)
14 | * * macOS upgrade 37% completed. Please wait.
15 | * Default behavior is to NOT restart, but enabling that option on a pkg in Jamf will ensure particular upgrade(s) restarts the mac.
16 | * Supports both Intel and Apple Silicon macs and all their little foibles.
17 | * Uses familiar Jamf Pro constructs such as triggers, smart groups and policies.
18 | * Local record of deferments used, last update time and last number of deferrments used for that update.
19 | This is readable via standard defaults read commands in the supplied extension attributes.
20 |
21 | ### Video Examples
22 |
23 | Example User Dialogs
24 | Shows time outs, deferrments and eventual force patching.
25 |
26 | https://user-images.githubusercontent.com/5807892/118586786-38043a80-b750-11eb-8dd6-540ffea0e857.mp4
27 |
28 |
29 | Patching Dialog (sped up)
30 | Shows screen lock out, application updating and macOS upgrading.
31 |
32 | Please note this is from an older version and there's more accurate progress bar on Application upgrades now!
33 |
34 | https://user-images.githubusercontent.com/5807892/118586556-d512a380-b74f-11eb-8411-cb52f067da86.mp4
35 |
36 |
37 | Requirements
38 | ------------
39 | * macOS 12 and above
40 | * Jamf Pro. v10 is preferred but we're not using anything version specific.
41 | * Cloud Distribution point or On-Prem HTTP distribution server(s).
42 | * Progress Bar requires the [swift-progress](https://github.com/adriannier/swift-progress) app from [Adrian Nier](https://github.com/adriannier).
43 |
44 | How to use
45 | ----------
46 |
47 | 1) Make sure your deployment pkgs have the format "name-version". e.g. "Cyberduck-7.9.0.pkg"
48 | This ensures the app processing script will function. You will get an error if it can't parse the name!
49 | 2) Download the latest swift-progress release from the link above.
50 | 3) Make a signed deployment pkg with the files and folders in the "Package" folder.
51 | 4) Check then upload that pkg to your Jamf Pro instance. You will deploy this via existing mechanisms and/or deploy processes to your fleet.
52 | 5) Implement the Extension Attributes to aid in your reporting and audits.
53 | 6) Implement the provided Configuration Profile so that PPPC will not interfere with this script. (Customise for your own corporate overlords!)
54 | 7) Upload the main scripts to your Jamf Pro instance.
55 | 8) The cached pkg processor script can utilise the parameter four value. Call this "OS Installer (blank for no)".
56 | 9) The main patcher installer script can also utilise parameter four for Self Service runs. Call this "Self Service policy (blank for no)".
57 |
58 | API User Required
59 | -----------------
60 |
61 | Create a user account on your Jamf Pro instance. Give it an appropriate username and complex password. The ONLY permission that it requires is read access to "Packages".
62 |
63 | The cached pkg processor will need those credentials in order to properly process the cached installer.
64 |
65 | Jamf Pro Extension Attributes
66 | -----------------------------
67 |
68 | Upload the three supplied extension attribute scripts. You may need to customise the folder path they are using.
69 |
70 | The EA's provide auditing information that may be useful such as current deferrals used, last update time and last number of deferrals used at last update.
71 |
72 | Jamf Pro Smart Groups
73 | ---------------------
74 |
75 | ##### Example Application Update Group
76 |
77 | You will need one of these per application. In this example, I'll be using CyberDuck.
78 |
79 | * **Name:** `Update Cyberduck 7.9.0`
80 | * **Criteria 1:** `"Application Title"` `"is"` `"Cyberduck.app"`
81 | * **Criteria 2:** `"and"` `"Application Version"` `"is not"` `"7.9.0"`
82 | * **Criteria 3:** `"and"` `"Cached Packages"` `"does not have"` `"Cyberduck-7.9.0.pkg"`
83 |
84 | What this does is to see if CyberDuck is installed, check the installed version and to then see if we've already cached an upgrade installer. That way we don't run unnecessarily.
85 |
86 | Every time you update an application in Jamf, the appropriate group **must** be updated as well!
87 |
88 | ##### Example macOS Update Group
89 |
90 | This is a special case and is not formatted like the other groups. For this example, macOS 11.3.1 is the latest version being offered.
91 |
92 | * **Name:** `Update macOS Installer`
93 | * **Criteria 1:** `"Operating System Version"` `"less than"` `"11.3.1"`
94 | * **Criteria 2:** `"Application Title"` `"does not have"` `"Install macOS Big Sur.app"`
95 |
96 | Jamf Pro Scripts
97 | ----------------
98 |
99 | ##### main patcher installer.sh
100 |
101 | This requires the simplest policy of all, as it's called every two hours by the LaunchDaemon in the deployment pkg.
102 |
103 | * **Name:** `Run Patcher`
104 | * **Trigger:** `Custom`
105 | * **Trigger event:** `apppatch`
106 | * **Execution Frequency:** `ongoing`
107 | * **Scripts:** `Set this to run the main patcher installer script`
108 | * **Maintenance:** `Enable Update Inventory`
109 |
110 | This is the script that does all the user prompting, displays the progress bars and performs clean up.
111 |
112 | (OPTIONAL) Duplicate this policy, make it a Self Service policy and ensure parameter 4 is set to yes. That will provide a better user experience for manual updating.
113 |
114 | ##### cached pkg processor.sh
115 |
116 | Once again, we'll use Cyberduck as an example but you'll have to create this per application.
117 |
118 | * **Name:** `Cache Cyberduck`
119 | * **Trigger:** `Recurring Check-in`
120 | * **Execution Frequency:** `ongoing`
121 | * **Packages:** `Set this to **cache** Cyberduck-7.9.0.pkg`
122 | * **Scripts:** `Set this to run the cached pkg processor script`
123 | * **Maintenance:** `Enable Update Inventory`
124 |
125 | Scope any policies using this to the appropriate smart group for that application you created earlier.
126 |
127 | ##### download macos installer.sh
128 |
129 | This again is a special case because the script is using softwareupdate to download the Install macOS app bundle directly to the Applications folder. As a result the policy is different to caching an application installer.
130 |
131 | * **Name:** `Cache macOS Installer`
132 | * **Trigger:** `Recurring Check-in`
133 | * **Execution Frequency:** `ongoing`
134 | * **Scripts:** `Run the download macos installer script as "Before"`
135 | * **Scripts:** `Set this to run the cached pkg processor script as "After" and setting script parameter 4 to "yes"`
136 | * **Maintenance:** `Enable Update Inventory`
137 |
138 | Scope this policy to the macOS update group from earlier.
139 |
140 | The script will attempt to download the latest macOS installer (caching server really useful here) and the special setting on the cached pkg script will cause that to look for an installer app bundle instead of a cached pkg.
141 |
142 | ### Enjoy! ###
143 |
144 | ### Special Thanks ###
145 |
146 | [Lachlan Stewart](https://github.com/loceee). Without [patchoo](http://patchoo.github.io/patchoo) to inspire (and borrow ideas), this wouldn't exist.
147 |
148 | [Joshua Roskos](https://github.com/kc9wwh). Your work on macOS startosinstall was invaluable and this is a massive extension on your work.
149 |
--------------------------------------------------------------------------------