Categories
Mac admin'ing

Using a full macOS installer with Munki to patch macOS

Caveat

This works right now, but I don’t know if this will keep working in perpetuity. As Apple plans on removing enterprise options for macOS software update points out, being able to manage patches in macOS is a moving target, and Apple is increasingly making it difficult for administrators to manage patches effectively.

Shoutout

Shoutout to Rod Christiansen on the MacAdmins Slack for putting this strange (but still working for now) method of patching on my radar.

Why would you want to do this?

Recently, softwareupdate has become an increasingly unreliable way to install updates. Munki 5 recently brought in some changes to have Managed Software Center nudge users to install patches via System Preferences (more details at Manual Apple Updates in Munki 5), similar to what Nudge does.

That is still the most reliable way to get patches installed, but what if you have Macs that basically sit unattended, so Nudge or a nudge-like prompt from Managed Software Center will basically be useless?

Well, apparently, you can use the full macOS installer to automate patching.

Limitations and considerations

One huge limitation, of course, is that you can probably use this method to patch only the latest macOS release. In other words, as of this writing, the latest build available for macOS 10.14.6 is 18G5033, but the latest full downloader for 10.14.6 is build 18G103. So you can’t really use an 18G103 full installer to update a Mojave client to 18G5033.

I believe using the installer to patch is the equivalent of doing a non-destructive reinstall of macOS (which you usually do in recovery mode), which means the download is considerably bigger than a regular patch download (more like 8 GB instead of 1 GB), and the install time is also longer (35-60 minutes instead of 20-30 minutes).

If the machines you’re patching are FileVault encrypted, you’ll still have to have someone be present either before or after the full macOS install to either do an authorized restart beforehand or a manual unlocking of FileVault afterwards.

Tweaks to the pkginfo

When you import something like Install macOS Catalina.app into Munki, munkiimport will, by default, make the installer_type into startosinstall. You may still want that installer in your Munki repo so you can upgrade Mojave clients to Catalina, so keep that pkginfo, but also make a copy of that pkginfo and tweak things a bit.

Change the installer type to be a CopyFromDMG:
    <key>installer_type</key>
    <string>copy_from_dmg</string>
    <key>items_to_copy</key>
    <array>
        <dict>
            <key>destination_path</key>
            <string>/tmp</string>
            <key>source_item</key>
            <string>Install macOS Catalina.app</string>
        </dict>
    </array>

and then have a postinstall script that runs the startosinstall command after copying from the .dmg:     <key>postinstall_script</key>
    <string>#!/bin/zsh
# Run startosinstall
/tmp/Install\ macOS\ Catalina.app/Contents/Resources/startosinstall --agreetolicense
    </string>

You’ll also want to change the name, so it’s distinct from Install_macOS_Catalina:

    <key>name</key>
    <string>Update_macOS_Catalina</string>

Might as well update the display name as well:

    <key>display_name</key>
    <string>Update macOS Catalina</string>

The last important tweak is to throw an installcheck_script in there so Munki knows whether the patch is installed or not:     <key>installcheck_script</key>
    <string>#!/usr/local/munki/python
from distutils.version import LooseVersion
import subprocess
import sys
# Desired OS build
desired='19F101'
def main():
    # Get current OS build
    cmd = [ '/usr/bin/sw_vers', '-buildVersion' ]
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8')
    out, err = p.communicate()
    if err:
        print("ERROR: Unable to determine current OS build. Considering installed for now...")
        sys.exit(1)
    else:
        # Strip out extra carriage return from output
        curr_build = out.strip()
        if LooseVersion(curr_build) &gt;= LooseVersion(desired):
            print("The current build {} is already greater than or equal to {}".format(curr_build, desired))
            sys.exit(1)
        else:
            print("Current build {} is not yet {}".format(curr_build, desired))
            sys.exit(0)
if __name__ == "__main__":
    main()
    </string>

Manifest Change

You may also want to add in a Munki conditional item so that only 10.15 clients are targeted.

    <key>conditional_items</key>
    <array>
        <dict>
            <key>condition</key>
            <string>os_vers_minor == 15</string>
            <key>managed_installs</key>
            <array>
                <string>Update_macOS_Catalina</string>
            </array>
        </dict>
    </array>

What the update process looks like

    Evaluating predicate: os_vers_minor == 15
    Predicate os_vers_minor == 15 is True
    * Processing manifest item Update_macOS_Catalina for install
    Looking for detail for: Update_macOS_Catalina, version latest...
    Considering 1 items with name Update_macOS_Catalina from catalog testing
    Considering item Update_macOS_Catalina, version 10.15.5.19F101 with minimum Munki version required 3.0.0.3211
    Our Munki version is 5.0.0.4034
    Considering item Update_macOS_Catalina, version 10.15.5.19F101 with minimum os version required 10.15
    Our OS version is 10.15.5
    Considering item Update_macOS_Catalina, version 10.15.5.19F101 with maximum os version supported 10.15.99
    Our OS version is 10.15.5
    Found Update_macOS_Catalina, version 10.15.5.19F101 in catalog testing
    Adding Update_macOS_Catalina to list of processed installs
    Running installcheck_script for Update_macOS_Catalina
    Current build 19F96 is not yet 19F101
    installcheck_script returned 0
    Need to install Update_macOS_Catalina

    The following items will be installed or upgraded:
        + Update_macOS_Catalina-10.15.5.19F101
            Updates macOS version to 10.15.5, build 19F101
         *Restart required

A key piece from /var/log/install.log: 2020-06-02 09:30:54-07 HOSTNAME softwareupdate[401]: Starting softwareupdate CLI tool
2020-06-02 09:30:54-07 HOSTNAME softwareupdated[402]: softwareupdated: Starting with build 10.15.5 (19F101)
2020-06-02 09:30:54-07 HOSTNAME softwareupdated[402]: authorizeWithEmptyAuthorizationForRights: Requesting provided rights: 1
2020-06-02 09:30:56-07 HOSTNAME softwareupdated[402]: /Library/Bundles does not exist - watching for directory creation
2020-06-02 09:30:56-07 HOSTNAME softwareupdated[402]: Previous System Version : 10.15.5 (19F96), Current System Version : 10.15.5 (19F101)

Yeah, that’s it. That whole install (not counting the download of the installer) took a full hour to complete. On another machine I tested with, it was closer to 35 minutes to complete. So, your mileage may vary.

Leave a Reply

Your email address will not be published. Required fields are marked *