Workspace ONE Delivers the MacOS Updater Utility (MUU): Does it Finally Solve the Patching Woes of WS1 Mac Software Updates?

Workspace ONE Delivers the MacOS Updater Utility (MUU): Does it Finally Solve the Patching Woes of WS1 Mac Software Updates?

scripting
Workspace ONE Delivers

Historically, I’m proud to say I have been one of the primary thought-leaders when it comes to MacOS management on Workspace ONE. As evident, from my many MacOS articles I’ve written over the last few years. One of the problems that I have struggled with is patching. Patching issues are nothing new with MacOS and Windows. We constantly see weird patching behaviors from caching, to getting updates to actually take, deferrals, and more. A new “utility” available now called the MacOS Updater Utility promises to change all of that. Let’s provide an overview of that utility, show the setup, user experience, and close with some thoughts.

What is the macOS Updater Utility?

VMware’s documentation on the MUU (some may call it MOUU) but I think MUU is more fun! Like this guy:

The MUU is comprised of a script and a profile along with some fancy API magic. Basically, it’s an enhancement on the software update capabilities within MacOS. Overall, the Software Updates on MacOS have always been wildly inconsistent. The workflow looks like this:

First, we will cover the commands and their code behind them. We do this to show how simple it is (in a good way) and its important to comprehend it:

The Steps in the MUU Flow

1: The script will execute every X hours (let’s call it 4 hours).

2: It checks to verify if the MUU device profile is installed:


if [ ! -f "$managedPlist" ]; then
  #clean up counter file and Exit
  rm -rf "$counterFile"
  log "config profile not installed, exiting....."
  /bin/cp "$logLocation" "$ws1Log"
  exit 0
fi
log "profile installed"

3: Next, it checks to see if the OS is current:


desiredOS=$(/usr/libexec/PlistBuddy -c "Print :desiredOSversion" "$managedPlist")
if [[ $(version $currentOS) -ge $(version $desiredOS) ]]; then
  #clean up counter file and Exit
  rm -rf "$counterFile"
  log "device is up to date, exiting....."
  /bin/cp "$logLocation" "$ws1Log"
  exit 0
fi
log "upgrade needed - currentOS: $currentOS : desiredOS: $desiredOS"

4: Now, check if the update is already downloaded:

    log "checking for minor update download"
    #check directory exists
    dirCount=$(find /System/Library/AssetsV2/com_apple_MobileAsset_MacSoftwareUpdate -maxdepth 1 -type d | /usr/bin/wc -l)
    if [[ "$dirCount" -gt 1 ]]; then
      #check for matching OS version
      index=1
      while [ $index -lt $dirCount ]
      do
        index=$((index+1))
        updateDir=$(find /System/Library/AssetsV2/com_apple_MobileAsset_MacSoftwareUpdate -maxdepth 1 -type d | /usr/bin/awk 'NR=='$index'{print}')
        msuPlist="$updateDir/Info.plist"
        msuOSVersion=$(/usr/libexec/PlistBuddy -c "Print :MobileAssetProperties:OSVersion" "$msuPlist")
        if [[ $(version $msuOSVersion) -eq $(version $desiredOS) ]];  then
          log "Download found"
          echo "yes"
          return
        fi
      done
      log "Download found but not correct"
      echo "no"
    else
      #download not started
      log "Download not found"
      echo "no"
    fi
  fi
}

5: Initiate the Installer Download:

dlInstaller () {
  #check major or minor update
  if [[ "$1" = "major" ]]; then
    #download major OS Installer
    /usr/sbin/softwareupdate --fetch-full-installer --full-installer-version "$desiredOS" &
  else
    #check if need to use ProductKey or ProductVersion (macOS 12+) in MDM command
    if [[ "$currentMajor" -ge "12" ]]; then
      #use productVersion
      log "mdmCommand DownloadOnly ProductVersion $desiredOS"
      mdmCommand "DownloadOnly" "ProductVersion" "$desiredOS"
    else
      #use productKey
      log "mdmCommand DownloadOnly ProductKey $desiredProductKey"
      mdmCommand "DownloadOnly" "ProductKey" "$desiredProductKey"
    fi
  fi
  #echo "Downloading"
}

6: Check if active user is logged in:

if [[ "$currentUser" = "root" ]]; then exit 0; fi
log "$currentUser is logged in"

#check if settings profile is Installed
if [ ! -f "$managedPlist" ]; then
  #clean up counter file and Exit
  rm -rf "$counterFile"
  log "config profile not installed, exiting....."
  /bin/cp "$logLocation" "$ws1Log"
  exit 0
fi
log "profile installed"

7: Send Notification to User:

buttonLabel=$(/usr/libexec/PlistBuddy -c "Print :buttonLabel" "$managedPlist")
if [[ "$buttonLabel" == "" ]]; then buttonLabel="Upgrade"; fi

#check if user has deferrals remaining
if [[ $deferralCount -lt  $maxDeferrals ]]; then
  #prompt user to upgrade with deferral option
  log "prompting user with deferral"
  userReturn=$(userPrompt "deferral")
  #check user response
  if [ "$userReturn" = "button returned:$buttonLabel, gave up:false" ]; then
    #trigger update and exit
    log "installing update"
    /usr/bin/caffeinate -t 7200 & # prevent sleep while installing update
    cpuType=$(/usr/sbin/sysctl -n machdep.cpu.brand_string | grep -o "Intel")
    if [[ "$updateType" == "major" &&  -n "$cpuType" ]]; then
      installUpdate "$updateType"
    else
      response=$(installUpdate "$updateType")
      

8: Did User Defer?

      if [[ "$response" == "no" ]]; then
        log "API command to install update failed, exiting....."
        /bin/cp "$logLocation" "$ws1Log"
        exit 0
      fi
    fi
    #trigger script to notify user that upgrade is installing and reboot is imminent
    log "triggering notification script"
    installStatus
  else
    #increase deferral count and exit
    log "user deferred"
    deferralCount=$((deferralCount+1))
    /usr/bin/defaults write "$counterFile" deferralCount -int $deferralCount
  fi
else

9: Proceed with the Update:

 #prompt user that upgrade will take place momentarily - close out of all programs, save work, etc.
  log "prompting user without deferral"
  userPrompt "force"
  #trigger update
  log "installing update"
  /usr/bin/caffeinate -t 7200 & # prevent sleep while installing update
  cpuType=$(/usr/sbin/sysctl -n machdep.cpu.brand_string | grep -o "Intel")
  if [[ "$updateType" == "major" &&  -n "$cpuType" ]]; then
    installUpdate "$updateType"
  else
    response=$(installUpdate "$updateType")
    if [[ "$response" == "no" ]]; then
      log "API command to install update failed, exiting....."
      /bin/cp "$logLocation" "$ws1Log"
      exit 0
    fi
  fi
  #trigger script to notify user that upgrade is installing and reboot is imminent
  log "triggering notification script"
  installStatus
fi

The Install Update Function: 
installUpdate () {
  #check major or minor update
  if [[ "$1" = "major" ]]; then
    #install major update
    #check if need to use ProductKey or ProductVersion (macOS 12+) in MDM command
    if [[ "$currentMajor" -ge "12" ]]; then
      #use productVersion
      log "mdmCommand InstallASAP ProductVersion $desiredOS"
      mdmCommand "InstallASAP" "ProductVersion" "$desiredOS"
    else
      #use productKey - check intel vs apple silicon as well
      cpuType=$(/usr/sbin/sysctl -n machdep.cpu.brand_string | grep -o "Intel")
      if [[ -n "$cpuType" ]]; then
        #intel - use startosinstall
        log "Triggering update with startosinstall"
        case $desiredMajor in
          "11")
            /Applications/Install\ macOS\ Big\ Sur.app/Contents/Resources/startosinstall --agreetolicense --nointeraction --forcequitapps &
            ;;
          "12")
            /Applications/Install\ macOS\ Monterey.app/Contents/Resources/startosinstall --agreetolicense --nointeraction --forcequitapps &
            ;;
          "13")
            /Applications/Install\ macOS\ Ventura.app/Contents/Resources/startosinstall --agreetolicense --nointeraction --forcequitapps &
            ;;
          *)
            echo "unknown major version"
            ;;

The MDM command function:

# MDM command via api
# $1 - InstallAction, $2 - ProductKey or ProductVersion, $3 - productKey/version data
mdmCommand () {
  # custom MDM command API
  response=$(/usr/bin/curl "$apiURL/api/mdm/devices/commands?command=CustomMdmCommand&searchby=SerialNumber&id=$serial" \
  -X POST \
  -H "Authorization: Bearer $authToken" \
  -H "Accept: application/json;version=2" \
  -H "Content-Type: application/json" \
  -d '{"CommandXML" : "<dict><key>RequestType</key><string>ScheduleOSUpdate</string><key>Updates</key><array><dict><key>InstallAction</key><string>'$1'</string><key>'$2'</key><string>'$3'</string></dict></array></dict>"}')
  log "API call sent - serial: $serial, action: $1, type: $2, value: $3"
  log "API Response: $response"
  if [[ ! -z "$response" ]]; then
    #api failed
    echo "no"
    log "Failed to send MDM command via API"
    return
  fi
  echo "command sent"
}

Comments on the MUU Flow

So we completed covering the flow. I showed the code so you could understand that it’s not overly complicated. They are using well-known capabilities like “softwareupdate” and initiating MDM commands via REST API. The concepts are really solid but they’re not over-the-top which I can appreciate. The value in that is you could easily modify this code yourself if you wanted to do something more significant or even potentially enhance the code that Matt Zaske was nice enough to deliver.

Requirements for MUU

There are two key requirements for MUU:

  • Workspace ONE UEM version 22.04 or later
  • Apple device running macOS version 11.7.0 (Big Sur) or later

The tool has been tested on both MacOS chipsets. The requirements aren’t too crazy. It’s important to point out that it isn’t a supported solution but just something that was built similar to a Fling.

The other thing to point out is that the requirements for it to kick off are:

  • An active user must be logged into the device
  • The current OS version must be less than the desired OS version configured in the device profile

Let’s go into how things get set up by starting with the Workspace ONE UEM Setup for the Update Utility.

Configuring the MacOS Update Utility on Workspace ONE UEM

First, we need to setup the WS1 UEM OAuth Client, which you can see in this video below:

Next, we need to grab the script from Matt’s Github, and build/deploy the script via Workspace ONE UEM as seen in the video below. When you build it, don’t forget to get the right token script:

The trifecta is achieved by building/deploying the MDM profile seen in this video:

Let’s discuss the Custom XML real quick to provide some insight:

<dict>
  <key>desiredOSversion</key>
  <string>12.6.1</string>
  <key>promptTimer</key>
  <string>300</string>
  <key>maxDeferrals</key>
  <integer>10</integer>
  <key>buttonLabel</key>
  <string>Upgrade</string>
  <key>messageIcon</key>
  <string>/usr/local/borat.icns</string>
  <key>messageTitle</key>
  <string>Wawaweewa! Patches!</string>
  <key>messageBody</key>
  <string>New patch is ready! Very Nice!</string>
  <key>PayloadIdentifier</key>
  <string>com.macOSupdater.settings.B11EB268-A733-4BE3-8CF2-9989344E26C0</string>
  <key>PayloadUUID</key>
  <string>B11EB268-A733-4BE3-8CF2-9989344E26C0</string>
  <key>PayloadOrganization</key>
  <string>Workspace ONE</string>
  <key>PayloadType</key>
  <string>com.macOSupdater.settings</string>
  <key>PayloadVersion</key>
  <integer>1</integer>
  <key>PayloadDisplayName</key>
  <string>macOS Updater Settings</string>
  <key>PayloadDescription</key>
  <string>Settings to control macOS Updater behavior</string>
</dict>

I just want to explain the key items within this:

  • desiredOSVersion: Specifies the OS Version you want to target (Can be a major or minor version e.g. 12.6.1 or 13.0)
  • promptTimer: how long in seconds before the prompt times out
  • maxDeferrals: how many times you can defer the installation of new patches
  • buttonLabel: what you want the upgrade button to say
  • messageIcon: what icon you want to appear on the pop-up
  • messageTitle: the title of the upgrade message
  • messageBody: the body of the upgrade message

Let’s finish this up with the code you can use to create the .icns file (the format needed for the icon):

##Set Input and Variables##
input_filepath="borat.png" 
output_iconset_name="borat.iconset"
mkdir $output_iconset_name

##Create the Various File Sizes##
sips -z 16 16     $input_filepath --out "${output_iconset_name}/icon_16x16.png"
sips -z 32 32     $input_filepath --out "${output_iconset_name}/icon_16x16@2x.png"
sips -z 32 32     $input_filepath --out "${output_iconset_name}/icon_32x32.png"
sips -z 64 64     $input_filepath --out "${output_iconset_name}/icon_32x32@2x.png"
sips -z 128 128   $input_filepath --out "${output_iconset_name}/icon_128x128.png"
sips -z 256 256   $input_filepath --out "${output_iconset_name}/icon_128x128@2x.png"
sips -z 256 256   $input_filepath --out "${output_iconset_name}/icon_256x256.png"
sips -z 512 512   $input_filepath --out "${output_iconset_name}/icon_256x256@2x.png"
sips -z 512 512   $input_filepath --out "${output_iconset_name}/icon_512x512.png"

iconutil -c icns $output_iconset_name

The MacOS Updater Utility User Experience

Now that we have spent a LARGE amount of time showing code and blah blah, we should see what the user experience looks like:

The great thing with this utility as you have seen, you can fully customize your user experience down to each minor detail. My best suggestion is to not be too heavy handed because creating nonsensical pop-ups will ruin the good will that you are trying to achieve with this platform. Let’s finish up with our final thoughts on the solution.

Mobile Jon’s Thoughts on the MUUUUUUUUUU!!!

So, let’s cut the bull and get to the nitty gritty of things. Overall, I think the utility is a major step-up. My testing thus far has been a bit inconsistent. In one or two of my tests it didn’t pickup on an update that was already downloaded, but a removal and reinstall of the profile definitely helped things out quite a bit.

The good thing is you can focus on a few logs to troubleshoot things appropriately. The path of the log is:

/Library/Application Support/AirWatch/Data/ProductsNew/macOSupdater.log (provides the log of the tool itself)

/var/log/install.log (the software update log)

A few things that I would say constructively that need to be improved:

  • We need better logging in the mcOSupdater.log which isn’t the most clear at times, which you could definitely do by enhancing the script ( I may try to fork this at some point).
  • Error catching could be a bit more effective or possibly some commands to purge MacOS update cache. It appears that it tries to queue up the download and just dies on the vine, but I think overall its not a huge deal, but I think we could do better there.

My final thoughts here are that I really like the solution. It’s easy to implement and the value proposition is obvious. I think that this solution has a ton of potential, but VMware NEEDS to make this a fully supported solution since Microsoft Intune is now offering it as part of their solution.

Facebook
Twitter
LinkedIn

Categories

Social Media

Get The Latest Updates

Subscribe To Our Weekly Newsletter

No spam, notifications only about the latest posts and updates.