Re-ordering SSIDs on Mac with Munki

You might want to set your corporate network as the first preferred network on Mac OS. Unfortunately there is no built in command line tool that can do this. It can be done in python though with the CoreWLAN framework and the Objective-C Bridge. Unfortunately Apple has decided not to continue providing python on the Mac so we will need a third party python package. If you are using Munki to manage software then you are in luck! Munki’s included python package includes the Objective-C Bridge.

We will need two scripts to make this work, one to check if we should re-order the networks and one to actually do the work. In munki create a nopkg and include these scripts:

install-check:

#!/usr/local/munki/munki-python

# As written, this requires the following:
# - OS X 10.6+ (has been reported working through OS 12)
# - python 3.x
# - pyObjC (as such, recommended to be used with munki's python)

# Run with root
# exits 0 if order is wrong and 1 if order is right.

import objc, ctypes.util, os.path, collections
from Foundation import NSOrderedSet

# List of preferred SSIDs in priority order - edit/add/delete as needed
PreferredSSIDs = ["MySSID"]

def load_objc_framework(framework_name):
    # Utility function that loads a Framework bundle and creates a namedtuple where the attributes are the loaded classes from the Framework bundle
    loaded_classes = dict()
    framework_bundle = objc.loadBundle(framework_name, bundle_path=os.path.dirname(ctypes.util.find_library(framework_name)), module_globals=loaded_classes)
    # Exclude class entries that start with an underscore
    loaded_classes = dict(x for x in loaded_classes.items() if (not x[0].startswith('_')))
    return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)

# Load the CoreWLAN.framework (10.6+)
CoreWLAN = load_objc_framework('CoreWLAN')

# Load all available wifi interfaces
interfaces = dict()
for i in CoreWLAN.CWInterface.interfaceNames():
    interfaces[i] = CoreWLAN.CWInterface.interfaceWithName_(i)

# Repeat the configuration with every wifi interface
for i in interfaces.keys():
    # Grab a mutable copy of this interface's configuration
    configuration_copy = CoreWLAN.CWMutableConfiguration.alloc().initWithConfiguration_(interfaces[i].configuration())
    # Find all the preferred/remembered network profiles
    profiles = list(configuration_copy.networkProfiles().array())
    # Grab all the SSIDs, in order
    SSIDs = [x.ssid() for x in profiles]
    # Loop through PreferredSSIDs list and mathces the order of preferred networks
    for x in range(len(PreferredSSIDs)):
      if (PreferredSSIDs[x] != profiles[x].ssid()):
        exit(0)

exit(1)

post-install:

#!/usr/local/munki/munki-python

# As written, this requires the following:
# - OS X 10.6+ (has been reported working through OS 12)
# - python 3.x
# - pyObjC (as such, recommended to be used with munki's python)

# Run with root

import objc, ctypes.util, os.path, collections
from Foundation import NSOrderedSet

# List of preferred SSIDs in priority order - edit/add/delete as needed
PreferredSSIDs = ["MySSID"]

def load_objc_framework(framework_name):
    # Utility function that loads a Framework bundle and creates a namedtuple where the attributes are the loaded classes from the Framework bundle
    loaded_classes = dict()
    framework_bundle = objc.loadBundle(framework_name, bundle_path=os.path.dirname(ctypes.util.find_library(framework_name)), module_globals=loaded_classes)
    # Exclude class entries that start with an underscore
    loaded_classes = dict(x for x in loaded_classes.items() if (not x[0].startswith('_')))
    return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)

# Load the CoreWLAN.framework (10.6+)
CoreWLAN = load_objc_framework('CoreWLAN')

# Load all available wifi interfaces
interfaces = dict()
for i in CoreWLAN.CWInterface.interfaceNames():
    interfaces[i] = CoreWLAN.CWInterface.interfaceWithName_(i)

# Repeat the configuration with every wifi interface
for i in interfaces.keys():
    # Grab a mutable copy of this interface's configuration
    configuration_copy = CoreWLAN.CWMutableConfiguration.alloc().initWithConfiguration_(interfaces[i].configuration())
    # Find all the preferred/remembered network profiles
    profiles = list(configuration_copy.networkProfiles().array())
    # Grab all the SSIDs, in order
    SSIDs = [x.ssid() for x in profiles]
    # Loop through PreferredSSIDs list in reverse order sorting each entry to the front of profiles array so it
    # ends up sorted with PreferredSSIDs as the first items.
    # Order is preserved for other SSIDs, example where PreferredSSIDs is [ssid3, ssid4]:
    #    Original: [ssid1, ssid2, ssid3, ssid4]
    #   New order: [ssid3, ssid4, ssid1, ssid2]
    for aSSID in reversed(PreferredSSIDs):
        profiles.sort(key=lambda x: x.ssid() == aSSID, reverse=True)
    # Now we have to update the mutable configuration
    # First convert it back to a NSOrderedSet
    profile_set = NSOrderedSet.orderedSetWithArray_(profiles)
    # Then set/overwrite the configuration copy's networkProfiles
    configuration_copy.setNetworkProfiles_(profile_set)
    # Then update the network interface configuration
    result = interfaces[i].commitConfiguration_authorization_error_(configuration_copy, None, None)

Leave a comment