Skip to content

Instantly share code, notes, and snippets.

@pudquick
Created July 6, 2015 18:40
Show Gist options
  • Save pudquick/fcbdd3924ee230592ab4 to your computer and use it in GitHub Desktop.
Save pudquick/fcbdd3924ee230592ab4 to your computer and use it in GitHub Desktop.
#!/usr/bin/python
# As written, this requires the following:
# - OS X 10.6+ (may not work in 10.10, haven't tested)
# - python 2.6 or 2.7 (for collections.namedtuple usage, should be fine as default python in 10.6 is 2.6)
# - pyObjC (as such, recommended to be used with native OS X python install)
# Only tested and confirmed to work against 10.9.5
# Run with root
import objc, ctypes.util, os.path, collections
from Foundation import NSOrderedSet
preferred_SSID = 'This SSID Should Be First'
next_to_last_SSID = 'This SSID Should Be Next To Last'
last_SSID = 'This SSID Should be Last'
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)
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())
# Grab all the SSIDs, in order
SSIDs = [x.ssid() for x in profiles]
# Check to see if our preferred SSID is in the list
if (preferred_SSID in SSIDs):
# Apparently it is, so let's adjust the order
# Profiles with matching SSIDs will move to the front, the rest will remain at the end
# Order is preserved, example where 'ssid3' is preferred:
# Original: [ssid1, ssid2, ssid3, ssid4]
# New order: [ssid3, ssid1, ssid2, ssid4]
profiles.sort(key=lambda x: x.ssid() == preferred_SSID, reverse=True)
# Now we move next_to_last_SSID to the end
profiles.sort(key=lambda x: x.ssid() == next_to_last_SSID, reverse=False)
# Now we move last_SSID to the end (bumping next_to_last_SSID)
profiles.sort(key=lambda x: x.ssid() == last_SSID, reverse=False)
# 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)
@umeditor
Copy link

FWIW, this worked for me an MacOS Sierra (10.12.1).

@bwiessner
Copy link

Confirmed - works on 10.12.1 on new MacbokPro_touchbar

@franton
Copy link

franton commented Apr 11, 2018

Still works on 10.13.4 ;)

@franton
Copy link

franton commented Apr 11, 2018

I reworked your sort code a little, because I found an edge case where a device without a present WiFi service would cause the script to fail. I just encapsulated your sort code with an if check.

if CoreWLAN.CWInterface.interfaceNames():

    for i in CoreWLAN.CWInterface.interfaceNames():
        interfaces[i] = CoreWLAN.CWInterface.interfaceWithName_(i)

sorting code here

else:
	print "No WiFi Interfaces found"

@dstranathan
Copy link

This script doesn't handle missing SSIDs.

For example - I populated the vars with fake, non-existent SSIDs and the script exits cleanly as if it actually moved some SSIDs up/down in the list - when they don't exist at all.

Has anyone added logic to search for the target SSID(s) and exit with an error if they aren't in the Preferred Network list?

@skoobasteeve
Copy link

Do you know what the main blockers would be to porting this to Python 3? This script works great even in macOS 11, but I'm trying to plan for the future. Took a stab at it but the PyObjC stuff is completely foreign to me.

@cumcitjamfadmin
Copy link

@skoobasteeve DId you ever find a solution? I tried to do it myself, but got hung up on the line:

return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)

From what I could find, in python3 loaded_classes.keys() includes items that start with _ (underscores), while with python2 running the same command doesn't. The problem seems to be namedtuple doesn't play well with underscores, but I don't know enough about python3 to figure out how to account for it. It's also possible there'd be problems down the line with other parts of the script, but I can't get past that.

I'm using the macadmins python3, so it's able to import all the modules at the top of the script (that may not be the case for other python3 packages).

@cumcitjamfadmin
Copy link

cumcitjamfadmin commented Feb 11, 2022

I got in contact with the creator of the script and they advised making the following changes:

Add before line 22 (to exclude entries that start with an underscore):
loaded_classes = dict(x for x in loaded_classes.items() if (not x[0].startswith('_')))

Change what's currently line 37 to (I believe this was required based on PyObjC 7.0.1 being installed):
profiles = list(configuration_copy.networkProfiles().array())

After making those changes the script worked. If anyone's having issues, make sure it's macadmins python3 that's installed (currently it's 3.9.5 for me).

@skoobasteeve
Copy link

Got it working with the above changes! Just in time for the 12.3 update.

Thank you @cumcitjamfadmin and @pudquick for your work!

@drewjc75
Copy link

drewjc75 commented Apr 4, 2022

I was so excited to find this today because we want to implement a method of doing just this at my company—only to discover that as of 12.3, python2 is no longer packaged with Mac OS. And unfortunately I don't have enough experience with python to be able to rewrite for python3. :(

@skoobasteeve
Copy link

skoobasteeve commented Apr 4, 2022

I was so excited to find this today because we want to implement a method of doing just this at my company—only to discover that as of 12.3, python2 is no longer packaged with Mac OS. And unfortunately I don't have enough experience with python to be able to rewrite for python3. :(

@drew7579 See the modified script below. We have this working in our environment with a single preferred 802.1x SSID. Since there’s no built-in Python 3 in 12.3, it relies on macadmins-python.

#!/Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/bin/python3

# As written, this requires the following:
# - OS X 10.6+ (may not work in 10.10, haven't tested)
# - python 2.6 or 2.7 (for collections.namedtuple usage, should be fine as default python in 10.6 is 2.6)
# - pyObjC (as such, recommended to be used with native OS X python install)

# Only tested and confirmed to work against 10.9.5
# Run with root

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

preferred_SSID    = ''
next_to_last_SSID = ''
last_SSID         = ''

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)
    # ADDED FOR PYTHON 3 CHANGE
    # Avoids loading libraries 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
    # DEPRECATED PYTHON 2 CODE: profiles = list(configuration_copy.networkProfiles())
    # ADDED FOR PYTHON 3 CHANGE
    profiles = list(configuration_copy.networkProfiles().array())
    # Grab all the SSIDs, in order
    SSIDs = [x.ssid() for x in profiles]
    # Check to see if our preferred SSID is in the list
    if (preferred_SSID in SSIDs):
        # Apparently it is, so let's adjust the order
        # Profiles with matching SSIDs will move to the front, the rest will remain at the end
        # Order is preserved, example where 'ssid3' is preferred:
        #    Original: [ssid1, ssid2, ssid3, ssid4]
        #   New order: [ssid3, ssid1, ssid2, ssid4]
        profiles.sort(key=lambda x: x.ssid() == preferred_SSID, reverse=True)
        # Now we move next_to_last_SSID to the end
        profiles.sort(key=lambda x: x.ssid() == next_to_last_SSID, reverse=False)
        # Now we move last_SSID to the end (bumping next_to_last_SSID)
        profiles.sort(key=lambda x: x.ssid() == last_SSID, reverse=False)
        # 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)

@jleomcdo
Copy link

jleomcdo commented Apr 8, 2022

I'm a little new with python, but how do I install the python3 from MacAdmins Github? I have the files downloaded, but not sure how to install it.

@skoobasteeve
Copy link

@jleomcdo You can run this script with any python 3 you have locally, including the one you get with xcode-select --install.

That said, you can download the latest pkg from the releases page on macadmins/python and just double-click to install locally, though it's meant to be deployed to a user base with JAMF or another tool. Once it's installed, add the shebang that leads to the install to any scripts you deploy and it will use that version of Python to run the script.

@jleomcdo
Copy link

jleomcdo commented Apr 8, 2022 via email

@skoobasteeve
Copy link

@jleomcdo you can absolutely do that, it's exactly what we're doing at my org.

@natkoo
Copy link

natkoo commented Jun 28, 2022

Is there any script for monterey?

@drewjc75
Copy link

Is there any script for monterey?

The script @skoobasteeve posted a couple comments above works in Monterey, but requires the MacAdmins python3 package to be installed as Monterey doesn't have a default python installed. It works fine on all of our machines in our environment, including the ones running Monterey.

@natkoo
Copy link

natkoo commented Sep 9, 2022

Is there any script for monterey?

The script @skoobasteeve posted a couple comments above works in Monterey, but requires the MacAdmins python3 package to be installed as Monterey doesn't have a default python installed. It works fine on all of our machines in our environment, including the ones running Monterey.

So how do we install all this with JAMF? Without installing xcode?

@skoobasteeve
Copy link

Is there any script for monterey?

The script @skoobasteeve posted a couple comments above works in Monterey, but requires the MacAdmins python3 package to be installed as Monterey doesn't have a default python installed. It works fine on all of our machines in our environment, including the ones running Monterey.

So how do we install all this with JAMF? Without installing xcode?

Push the macadmins/python package to your machines. Once it's installed, you can run any Python3 script from JAMF by using a shebang at the top of the script that points to the new instance of Python:

#!/Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/bin/python3

@natkoo
Copy link

natkoo commented Sep 9, 2022

Oh now i get it - thanks so much for your help 👍

@mrmte
Copy link

mrmte commented Sep 20, 2022

Im trying this on Ventura. Installed python3 "python_recommended_signed-3.10.2.80694.pkg" when i run it im getting

Traceback (most recent call last):
File "/tmp/test2.py", line 18, in
CoreWLAN = load_objc_framework('CoreWLAN')
File "/tmp/test2.py", line 15, in load_objc_framework
return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)
File "/Library/ManagedFrameworks/Python/Python3.framework/Versions/3.10/lib/python3.10/collections/init.py", line 373, in namedtuple
raise ValueError('Type names and field names must be valid '
ValueError: Type names and field names must be valid identifiers: 'Foundation.__JSONDecoder'

@skoobasteeve
Copy link

@Tkimpton I think I got it working by adding a line to the load_objc_framework() function:

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)
    # ADDED FOR PYTHON 3 CHANGE
    # Avoids loading libraries that start with an underscore or reference a class e.g. Foundation.xx.
    loaded_classes = dict(x for x in loaded_classes.items() if (not x[0].startswith('_')))
    loaded_classes = dict(x for x in loaded_classes.items() if ('.' not in x[0]))
    # print(loaded_classes_clean.keys())
    return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)

The error occurs because namedtuple doesn't like class references in a list of strings, and there were keys in the loaded_classes dict with names like Foundation.xx and Swift.xx. I just removed all the items with keys that had a . character in them.

Can you try it and see if it resolves the issue?

@jstaubr
Copy link

jstaubr commented Nov 9, 2022

@skoobasteeve Works fine on Ventura here! Thank you
See further down

@magyarmelinda
Copy link

@jstaubr (CC: @skoobasteeve )
How did you manage to make it work on Ventura?
It works great on Monterey but on Ventura there were no change in the Preferred Network's list as I tested.
(It seems they deprecated the function. There's no UI either at System Preferences/Wi-Fi's to change the order of the networks. Only "Known Networks" window. )

Has anybody found a workaround for that?

@franton
Copy link

franton commented Nov 21, 2022

@daydreamheart I'm sorry to inform you that Apple has "automated" the preferred wifi SSID choice in Ventura. It's not likely this will work going forward.https://support.apple.com/en-us/HT202831

@jstaubr
Copy link

jstaubr commented Nov 21, 2022

@daydreamheart

Unfortunately @franton is right. I thought it was working because I was on the same network that I tried to make preferred and my CLI check returned the correct SSID.
I found out later last week that Ventura always returns the currently connected SSID as top of the preferred list via
networksetup -listpreferredwirelessnetworks en0

@skoobasteeve
Copy link

Well it was good while it lasted! At least in their new system they indicate that EAP networks get priority over WPA. Just wish we could retain some control.

@ScottyBeach
Copy link

Maybe some feedback to Apple is in order?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment