├── facts ├── __init__.py ├── admin_users.py ├── backtomymac_configured.py ├── local_user_dirs.py ├── gatekeeper_status.py ├── sip_status.py ├── filevault_status.py ├── crashplan_username.py ├── physical_or_virtual.py ├── mojave_upgrade_supported.py ├── bigsur_upgrade_supported.py ├── catalina_upgrade_supported.py ├── monterey_upgrade_supported.py └── macos_upgrade_supported.py ├── .gitignore ├── LICENSE.md ├── README.md └── munki_facts.py /facts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .DS_Store files! 2 | .DS_Store 3 | 4 | # .pyc files 5 | *.pyc 6 | -------------------------------------------------------------------------------- /facts/admin_users.py: -------------------------------------------------------------------------------- 1 | '''Get the list of admin users''' 2 | 3 | from __future__ import absolute_import, print_function 4 | 5 | import grp 6 | 7 | 8 | def fact(): 9 | '''Return the list of admin users for this machine''' 10 | return {'admin_users': grp.getgrgid(80).gr_mem} 11 | 12 | 13 | if __name__ == '__main__': 14 | print(fact()) 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this source code except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | https://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | -------------------------------------------------------------------------------- /facts/backtomymac_configured.py: -------------------------------------------------------------------------------- 1 | '''Return a boolean indicating if BackToMyMac is configured on this machine''' 2 | 3 | from __future__ import absolute_import, print_function 4 | 5 | from SystemConfiguration import SCDynamicStoreCopyValue 6 | 7 | 8 | def fact(): 9 | '''Return True if there is a value for SystemConfiguration's 10 | Setup:/Network/BackToMyMac key''' 11 | return {'backtomymac_configured': 12 | SCDynamicStoreCopyValue(None, 'Setup:/Network/BackToMyMac') 13 | is not None} 14 | 15 | 16 | if __name__ == '__main__': 17 | print(fact()) 18 | -------------------------------------------------------------------------------- /facts/local_user_dirs.py: -------------------------------------------------------------------------------- 1 | '''Get a list of user home directories under /Users''' 2 | 3 | from __future__ import absolute_import, print_function 4 | 5 | import os 6 | 7 | 8 | def fact(): 9 | '''Return the list of user home directories under /Users''' 10 | # skip_names should include any directories you wish to ignore 11 | skip_names = ['Deleted Users', 'Shared', 'admin'] 12 | user_dirs = [item for item in os.listdir('/Users') 13 | if item not in skip_names and not item.startswith('.')] 14 | return {'local_user_dirs': user_dirs} 15 | 16 | 17 | if __name__ == '__main__': 18 | print(fact()) 19 | -------------------------------------------------------------------------------- /facts/gatekeeper_status.py: -------------------------------------------------------------------------------- 1 | '''Get the current Gatekeeper status''' 2 | 3 | from __future__ import absolute_import, print_function 4 | 5 | import subprocess 6 | 7 | 8 | def fact(): 9 | '''Return the current Gatekeeper status''' 10 | try: 11 | proc = subprocess.Popen(['/usr/sbin/spctl', '--status'], 12 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text="UTF-8") 13 | stdout, _ = proc.communicate() 14 | except (IOError, OSError): 15 | stdout = 'Unknown' 16 | 17 | return {'gatekeeper_status': stdout.strip()} 18 | 19 | 20 | if __name__ == '__main__': 21 | print(fact()) 22 | -------------------------------------------------------------------------------- /facts/sip_status.py: -------------------------------------------------------------------------------- 1 | '''Get the current SIP status for the startup disk''' 2 | 3 | from __future__ import absolute_import, print_function 4 | 5 | import subprocess 6 | 7 | 8 | def fact(): 9 | '''Return the current SIP status for the startup disk''' 10 | try: 11 | proc = subprocess.Popen(['/usr/bin/csrutil', 'status'], 12 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text="UTF-8") 13 | stdout, _ = proc.communicate() 14 | except (IOError, OSError): 15 | stdout = 'Unknown' 16 | 17 | return {'sip_status': stdout.strip()} 18 | 19 | 20 | if __name__ == '__main__': 21 | print(fact()) 22 | -------------------------------------------------------------------------------- /facts/filevault_status.py: -------------------------------------------------------------------------------- 1 | '''Get the current FileVault status for the startup disk''' 2 | 3 | from __future__ import absolute_import, print_function 4 | 5 | import subprocess 6 | 7 | 8 | def fact(): 9 | '''Return the current FileVault status for the startup disk''' 10 | try: 11 | proc = subprocess.Popen(['/usr/bin/fdesetup', 'status'], 12 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text="UTF-8") 13 | stdout, _ = proc.communicate() 14 | except (IOError, OSError): 15 | stdout = 'Unknown' 16 | 17 | return {'filevault_status': stdout.strip()} 18 | 19 | 20 | if __name__ == '__main__': 21 | print(fact()) 22 | -------------------------------------------------------------------------------- /facts/crashplan_username.py: -------------------------------------------------------------------------------- 1 | '''Extract the current CrashPlan user from CrashPlan's config file''' 2 | 3 | from __future__ import print_function 4 | 5 | 6 | def fact(): 7 | '''Return CrashPlan user name''' 8 | cp_identity_file = '/Library/Application Support/CrashPlan/.identity' 9 | username = '' 10 | try: 11 | with open(cp_identity_file) as identity: 12 | for line in identity.readlines(): 13 | if line.startswith('username='): 14 | username = line.partition('=')[2].rstrip() 15 | break 16 | except (IOError, OSError): 17 | pass 18 | 19 | return {'crashplan_username': username} 20 | 21 | 22 | if __name__ == '__main__': 23 | print(fact()) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | munki_facts.py is an "admin-provided conditions" script for Munki as described [here.](https://github.com/munki/munki/wiki/Conditional-Items#admin-provided-conditions) 4 | 5 | munki_facts.py is designed to be modular, allowing more 'facts' to be easily added without duplicating all of the code required to insert the facts into the ConditionalItems.plist. 6 | 7 | Python modules that generate one or more facts live in the 'facts' subdirectory. They must have a 'fact()' function that returns a dictionary of keys and values. 8 | 9 | Several sample fact modules are included. 10 | 11 | ## Usage 12 | 13 | `munki_facts.py` and the `facts` directory should be installed in `/usr/local/munki/conditions`. 14 | `munki_facts.py` must be marked as executable. 15 | 16 | Munki will run `munki_facts.py` and insert any facts it generates into the dictionary of items that are used in [manifest `conditional_items` predicates](https://github.com/munki/munki/wiki/Conditional-Items) and in [`installable_condition` items in pkginfo files](https://github.com/munki/munki/wiki/Pkginfo-Files#installable_condition). 17 | 18 | Any additional fact modules (that you create or obtain from others) should be copied into the `/usr/local/munki/conditions/facts` directory. 19 | 20 | ## More facts 21 | 22 | See https://github.com/munki/munki-facts/wiki/Community-Facts 23 | -------------------------------------------------------------------------------- /munki_facts.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/munki/munki-python 2 | '''Processes python modules in the facts directory and adds the info they 3 | return to our ConditionalItems.plist''' 4 | 5 | from __future__ import absolute_import, print_function 6 | 7 | import importlib.util 8 | import os 9 | import plistlib 10 | import sys 11 | from xml.parsers.expat import ExpatError 12 | 13 | # pylint: disable=no-name-in-module 14 | from CoreFoundation import CFPreferencesCopyAppValue 15 | # pylint: enable=no-name-in-module 16 | 17 | 18 | def main(): 19 | # pylint: disable=too-many-locals 20 | '''Run all our fact plugins and collect their data''' 21 | module_dir = os.path.join(os.path.dirname(__file__), 'facts') 22 | facts = {} 23 | 24 | # find all the .py files in the 'facts' dir 25 | fact_files = [ 26 | os.path.splitext(name)[0] 27 | for name in os.listdir(module_dir) 28 | if name.endswith('.py') and not name == '__init__.py'] 29 | 30 | for name in fact_files: 31 | # load each file and call its fact() function 32 | file_path = os.path.join(module_dir, name + '.py') 33 | try: 34 | # Python 3.4 and higher only 35 | spec = importlib.util.spec_from_file_location(name, file_path) 36 | module = importlib.util.module_from_spec(spec) 37 | spec.loader.exec_module(module) 38 | facts.update(module.fact()) 39 | # pylint: disable=broad-except 40 | except BaseException as err: 41 | print(u'Error %s in file %s' % (err, file_path), file=sys.stderr) 42 | # pylint: enable=broad-except 43 | 44 | if facts: 45 | # Handle cases when facts return None - convert them to empty 46 | # strings. 47 | for key, value in facts.items(): 48 | if value is None: 49 | facts[key] = '' 50 | # Read the location of the ManagedInstallDir from ManagedInstall.plist 51 | bundle_id = 'ManagedInstalls' 52 | pref_name = 'ManagedInstallDir' 53 | managedinstalldir = CFPreferencesCopyAppValue(pref_name, bundle_id) 54 | conditionalitemspath = os.path.join( 55 | managedinstalldir, 'ConditionalItems.plist') 56 | 57 | conditional_items = {} 58 | # read the current conditional items 59 | if os.path.exists(conditionalitemspath): 60 | try: 61 | with open(conditionalitemspath, "rb") as file: 62 | conditional_items = plistlib.load(file) 63 | except (IOError, OSError, ExpatError) as err: 64 | pass 65 | 66 | # update the conditional items 67 | conditional_items.update(facts) 68 | 69 | # and write them out 70 | try: 71 | with open(conditionalitemspath, "wb") as file: 72 | plistlib.dump(conditional_items, file) 73 | except (IOError, OSError) as err: 74 | print('Couldn\'t save conditional items: %s' % err, file=sys.stderr) 75 | 76 | 77 | if __name__ == "__main__": 78 | sys.exit(main()) 79 | -------------------------------------------------------------------------------- /facts/physical_or_virtual.py: -------------------------------------------------------------------------------- 1 | '''Returns a fact to indicate if this is a physical or virtual machine''' 2 | 3 | # sysctl function by Michael Lynn 4 | # https://gist.github.com/pudquick/581a71425439f2cf8f09 5 | 6 | from __future__ import absolute_import, print_function 7 | 8 | import plistlib 9 | import subprocess 10 | 11 | from ctypes import CDLL, c_uint, byref, create_string_buffer 12 | from ctypes import cast, POINTER 13 | from ctypes.util import find_library 14 | 15 | libc = CDLL(find_library('c')) 16 | 17 | 18 | def sysctl(name, output_type=str): 19 | '''Wrapper for sysctl so we don't have to use subprocess''' 20 | if isinstance(name, str): 21 | name = name.encode('utf-8') 22 | size = c_uint(0) 23 | # Find out how big our buffer will be 24 | libc.sysctlbyname(name, None, byref(size), None, 0) 25 | # Make the buffer 26 | buf = create_string_buffer(size.value) 27 | # Re-run, but provide the buffer 28 | libc.sysctlbyname(name, buf, byref(size), None, 0) 29 | if output_type in (str, 'str'): 30 | return buf.value.decode('UTF-8') 31 | if output_type in (int, 'int'): 32 | # complex stuff to cast the buffer contents to a Python int 33 | if size.value == 4: 34 | return cast(buf, POINTER(c_int32)).contents.value 35 | if size.value == 8: 36 | return cast(buf, POINTER(c_int64)).contents.value 37 | if output_type == 'raw': 38 | # sysctl can also return a 'struct' type; just return the raw buffer 39 | return buf.raw 40 | 41 | 42 | def is_virtual_machine(): 43 | '''Returns True if this is a VM, False otherwise''' 44 | cpu_features = sysctl('machdep.cpu.features').split() 45 | return 'VMM' in cpu_features 46 | 47 | 48 | def get_machine_type(): 49 | '''Return the machine type: physical, vmware, virtualbox, parallels or 50 | unknown_virtual''' 51 | if not is_virtual_machine(): 52 | return 'physical' 53 | 54 | # this is a virtual machine; see if we can tell which vendor 55 | try: 56 | proc = subprocess.Popen(['/usr/sbin/system_profiler', '-xml', 57 | 'SPEthernetDataType', 'SPHardwareDataType'], 58 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 59 | output = proc.communicate()[0] 60 | try: 61 | plist = plistlib.readPlistFromString(output) 62 | except AttributeError: 63 | plist = plistlib.loads(output) 64 | br_version = plist[1]['_items'][0]['boot_rom_version'] 65 | if 'VMW' in br_version: 66 | return 'vmware' 67 | elif 'VirtualBox' in br_version: 68 | return 'virtualbox' 69 | else: 70 | ethernet_vid = plist[0]['_items'][0]['spethernet_vendor-id'] 71 | for i in ['0x1ab8', '0x1af4']: 72 | if i in ethernet_vid: 73 | return 'parallels' 74 | 75 | except (IOError, KeyError, OSError): 76 | pass 77 | 78 | return 'unknown_virtual' 79 | 80 | 81 | def fact(): 82 | '''Return our physical_or_virtual fact''' 83 | return {'physical_or_virtual': get_machine_type()} 84 | 85 | 86 | if __name__ == '__main__': 87 | print(fact()) 88 | -------------------------------------------------------------------------------- /facts/mojave_upgrade_supported.py: -------------------------------------------------------------------------------- 1 | '''Returns a fact to indicate if this machine can be upgraded to Mojave''' 2 | 3 | # Based on 4 | # https://github.com/hjuutilainen/adminscripts/blob/master/ 5 | # check-10.12-sierra-compatibility.py 6 | 7 | # sysctl function by Michael Lynn 8 | # https://gist.github.com/pudquick/581a71425439f2cf8f09 9 | 10 | # IOKit bindings by Michael Lynn 11 | # https://gist.github.com/pudquick/ 12 | # c7dd1262bd81a32663f0#file-get_platform-py-L22-L23 13 | 14 | 15 | from __future__ import absolute_import, print_function 16 | 17 | from ctypes import CDLL, c_uint, byref, create_string_buffer 18 | from ctypes import cast, POINTER 19 | from ctypes.util import find_library 20 | import os 21 | 22 | import objc 23 | 24 | from Foundation import NSBundle, NSString, NSUTF8StringEncoding 25 | 26 | # glue to call C and Cocoa stuff 27 | libc = CDLL(find_library('c')) 28 | IOKit_bundle = NSBundle.bundleWithIdentifier_('com.apple.framework.IOKit') 29 | 30 | functions = [("IOServiceGetMatchingService", b"II@"), 31 | ("IOServiceMatching", b"@*"), 32 | ("IORegistryEntryCreateCFProperty", b"@I@@I"), 33 | ] 34 | 35 | objc.loadBundleFunctions(IOKit_bundle, globals(), functions) 36 | 37 | 38 | def io_key(keyname): 39 | """Gets a raw value from the IORegistry""" 40 | return IORegistryEntryCreateCFProperty( 41 | IOServiceGetMatchingService( 42 | 0, IOServiceMatching(b"IOPlatformExpertDevice")), keyname, None, 0) 43 | 44 | 45 | def io_key_string_value(keyname): 46 | """Converts NSData/CFData return value to an NSString""" 47 | raw_value = io_key(keyname) 48 | return NSString.alloc().initWithData_encoding_( 49 | raw_value, NSUTF8StringEncoding 50 | ).rstrip('\0') 51 | 52 | 53 | def sysctl(name, output_type=str): 54 | '''Wrapper for sysctl so we don't have to use subprocess''' 55 | if isinstance(name, str): 56 | name = name.encode('utf-8') 57 | size = c_uint(0) 58 | # Find out how big our buffer will be 59 | libc.sysctlbyname(name, None, byref(size), None, 0) 60 | # Make the buffer 61 | buf = create_string_buffer(size.value) 62 | # Re-run, but provide the buffer 63 | libc.sysctlbyname(name, buf, byref(size), None, 0) 64 | if output_type in (str, 'str'): 65 | return buf.value.decode('UTF-8') 66 | if output_type in (int, 'int'): 67 | # complex stuff to cast the buffer contents to a Python int 68 | if size.value == 4: 69 | return cast(buf, POINTER(c_int32)).contents.value 70 | if size.value == 8: 71 | return cast(buf, POINTER(c_int64)).contents.value 72 | if output_type == 'raw': 73 | # sysctl can also return a 'struct' type; just return the raw buffer 74 | return buf.raw 75 | 76 | 77 | def is_virtual_machine(): 78 | '''Returns True if this is a VM, False otherwise''' 79 | cpu_features = sysctl('machdep.cpu.features').split() 80 | return 'VMM' in cpu_features 81 | 82 | 83 | def is_supported_model(): 84 | '''Returns False if model is in list of unsupported models, 85 | True otherwise''' 86 | non_supported_models = [ 87 | u'MacBookPro4,1', 88 | u'MacPro2,1', 89 | u'Macmini5,2', 90 | u'Macmini5,1', 91 | u'MacBookPro5,1', 92 | u'MacBookPro1,1', 93 | u'MacBookPro5,3', 94 | u'MacBookPro5,2', 95 | u'iMac8,1', 96 | u'MacBookPro5,4', 97 | u'MacBookAir4,2', 98 | u'Macmini2,1', 99 | u'iMac5,2', 100 | u'iMac11,3', 101 | u'MacBookPro8,2', 102 | u'MacBookPro3,1', 103 | u'Macmini5,3', 104 | u'MacBookPro1,2', 105 | u'Macmini4,1', 106 | u'iMac9,1', 107 | u'iMac6,1', 108 | u'Macmini3,1', 109 | u'Macmini1,1', 110 | u'MacBookPro6,1', 111 | u'MacBookPro2,2', 112 | u'MacBookPro2,1', 113 | u'iMac12,2', 114 | u'MacBook3,1', 115 | u'MacPro3,1', 116 | u'MacBook5,1', 117 | u'MacBook5,2', 118 | u'iMac11,1', 119 | u'iMac10,1', 120 | u'MacBookPro7,1', 121 | u'MacBook2,1', 122 | u'MacBookAir4,1', 123 | u'MacPro4,1', 124 | u'MacBookPro6,2', 125 | u'iMac12,1', 126 | u'MacBook1,1', 127 | u'MacBookPro5,5', 128 | u'iMac11,2', 129 | u'iMac4,2', 130 | u'Xserve2,1', 131 | u'MacBookAir3,1', 132 | u'MacBookAir3,2', 133 | u'MacBookAir1,1', 134 | u'Xserve3,1', 135 | u'iMac4,1', 136 | u'MacBookAir2,1', 137 | u'Xserve1,1', 138 | u'iMac5,1', 139 | u'MacBookPro8,1', 140 | u'MacBook7,1', 141 | u'MacBookPro8,3', 142 | u'iMac7,1', 143 | u'MacBook6,1', 144 | u'MacBook4,1', 145 | u'MacPro1,1', 146 | ] 147 | current_model = get_current_model() 148 | if not current_model or current_model in non_supported_models: 149 | return False 150 | else: 151 | return True 152 | 153 | 154 | def get_minor_system_version(): 155 | '''Returns 7 for Lion, 8 for Mountain Lion, etc''' 156 | darwin_version = int(os.uname()[2].split('.')[0]) 157 | return darwin_version - 4 158 | 159 | 160 | def is_supported_system_version(): 161 | '''Returns True if current macOS version is 10.7 through 10.13, 162 | False otherwise''' 163 | macos_minor_version = get_minor_system_version() 164 | if macos_minor_version >= 14: 165 | return False 166 | elif macos_minor_version >= 7: 167 | return True 168 | else: 169 | return False 170 | 171 | 172 | def get_board_id(): 173 | '''Returns our board-id''' 174 | return io_key_string_value("board-id") 175 | 176 | 177 | def get_current_model(): 178 | '''Returns model info''' 179 | return io_key_string_value("model") 180 | 181 | 182 | def is_supported_board_id(): 183 | '''Returns True if current board_id is in list of supported board_ids, 184 | False otherwise''' 185 | platform_support_values = ( 186 | u'Mac-06F11F11946D27C5', 187 | u'Mac-031B6874CF7F642A', 188 | u'Mac-CAD6701F7CEA0921', 189 | u'Mac-50619A408DB004DA', 190 | u'Mac-7BA5B2D9E42DDD94', 191 | u'Mac-473D31EABEB93F9B', 192 | u'Mac-AFD8A9D944EA4843', 193 | u'Mac-B809C3757DA9BB8D', 194 | u'Mac-7DF2A3B5E5D671ED', 195 | u'Mac-35C1E88140C3E6CF', 196 | u'Mac-77EB7D7DAF985301', 197 | u'Mac-2E6FAB96566FE58C', 198 | u'Mac-827FB448E656EC26', 199 | u'Mac-BE0E8AC46FE800CC', 200 | u'Mac-00BE6ED71E35EB86', 201 | u'Mac-4B7AC7E43945597E', 202 | u'Mac-5A49A77366F81C72', 203 | u'Mac-35C5E08120C7EEAF', 204 | u'Mac-FFE5EF870D7BA81A', 205 | u'Mac-C6F71043CEAA02A6', 206 | u'Mac-4B682C642B45593E', 207 | u'Mac-90BE64C3CB5A9AEB', 208 | u'Mac-66F35F19FE2A0D05', 209 | u'Mac-189A3D4F975D5FFC', 210 | u'Mac-B4831CEBD52A0C4C', 211 | u'Mac-FA842E06C61E91C5', 212 | u'Mac-FC02E91DDD3FA6A4', 213 | u'Mac-06F11FD93F0323C5', 214 | u'Mac-9AE82516C7C6B903', 215 | u'Mac-27ADBB7B4CEE8E61', 216 | u'Mac-6F01561E16C75D06', 217 | u'Mac-F60DEB81FF30ACF6', 218 | u'Mac-81E3E92DD6088272', 219 | u'Mac-7DF21CB3ED6977E5', 220 | u'Mac-937CB26E2E02BB01', 221 | u'Mac-3CBD00234E554E41', 222 | u'Mac-F221BEC8', 223 | u'Mac-9F18E312C5C2BF0B', 224 | u'Mac-65CE76090165799A', 225 | u'Mac-CF21D135A7D34AA6', 226 | u'Mac-F65AE981FFA204ED', 227 | u'Mac-112B0A653D3AAB9C', 228 | u'Mac-DB15BD556843C820', 229 | u'Mac-937A206F2EE63C01', 230 | u'Mac-77F17D7DA9285301', 231 | u'Mac-C3EC7CD22292981F', 232 | u'Mac-BE088AF8C5EB4FA2', 233 | u'Mac-551B86E5744E2388', 234 | u'Mac-A5C67F76ED83108C', 235 | u'Mac-031AEE4D24BFF0B1', 236 | u'Mac-EE2EBD4B90B839A8', 237 | u'Mac-42FD25EABCABB274', 238 | u'Mac-F305150B0C7DEEEF', 239 | u'Mac-2BD1B31983FE1663', 240 | u'Mac-66E35819EE2D0D05', 241 | u'Mac-A369DDC4E67F1C45', 242 | u'Mac-E43C1C25D4880AD6', 243 | ) 244 | board_id = get_board_id() 245 | return board_id in platform_support_values 246 | 247 | 248 | def fact(): 249 | '''Return our mojave_upgrade_supported fact''' 250 | if is_virtual_machine(): 251 | return {'mojave_upgrade_supported': True} 252 | if (is_supported_model() and is_supported_board_id() and 253 | is_supported_system_version()): 254 | return {'mojave_upgrade_supported': True} 255 | return {'mojave_upgrade_supported': False} 256 | 257 | 258 | if __name__ == '__main__': 259 | # Debug/testing output when run directly 260 | print('is_virtual_machine: %s' % is_virtual_machine()) 261 | print('get_current_model: %s' % get_current_model()) 262 | print('is_supported_model: %s' % is_supported_model()) 263 | print('get_minor_system_version: %s' % get_minor_system_version()) 264 | print('is_supported_system_version: %s' % is_supported_system_version()) 265 | print('get_board_id: %s' % get_board_id()) 266 | print('is_supported_board_id: %s' % is_supported_board_id()) 267 | print(fact()) 268 | -------------------------------------------------------------------------------- /facts/bigsur_upgrade_supported.py: -------------------------------------------------------------------------------- 1 | '''Returns a fact to indicate if this machine can be upgraded to bigsur''' 2 | 3 | # Based on 4 | # https://github.com/hjuutilainen/adminscripts/blob/master/ 5 | # check-10.12-sierra-compatibility.py 6 | 7 | # sysctl function by Michael Lynn 8 | # https://gist.github.com/pudquick/581a71425439f2cf8f09 9 | 10 | # IOKit bindings by Michael Lynn 11 | # https://gist.github.com/pudquick/ 12 | # c7dd1262bd81a32663f0#file-get_platform-py-L22-L23 13 | 14 | # Big Sur changed the structure of the OS installer drastically 15 | # Information on what boardIDs and Models that are supported is buried in the installer found here: 16 | # Install macOS Big Sur.app/Contents/SharedSupport/SharedSupport.dmg - mount this 17 | # /Volumes/Shared Support/com_apple_MobileAsset_MacSoftwareUpdate/da4c0b39d73549c809a57e9b9951e380b28b122d.zip - decompress this, the name of the zip will most likely change with every OS update. 18 | # da4c0b39d73549c809a57e9b9951e380b28b122d/AssetData/boot/PlatformSupport.plist 19 | 20 | 21 | from __future__ import absolute_import, print_function 22 | 23 | from ctypes import CDLL, c_uint, byref, create_string_buffer 24 | from ctypes import cast, POINTER 25 | from ctypes.util import find_library 26 | import os 27 | 28 | import objc 29 | 30 | from Foundation import NSBundle, NSString, NSUTF8StringEncoding 31 | 32 | # glue to call C and Cocoa stuff 33 | libc = CDLL(find_library('c')) 34 | IOKit_bundle = NSBundle.bundleWithIdentifier_('com.apple.framework.IOKit') 35 | 36 | functions = [("IOServiceGetMatchingService", b"II@"), 37 | ("IOServiceMatching", b"@*"), 38 | ("IORegistryEntryCreateCFProperty", b"@I@@I"), 39 | ] 40 | 41 | objc.loadBundleFunctions(IOKit_bundle, globals(), functions) 42 | 43 | 44 | def io_key(keyname): 45 | """Gets a raw value from the IORegistry""" 46 | return IORegistryEntryCreateCFProperty( 47 | IOServiceGetMatchingService( 48 | 0, IOServiceMatching(b"IOPlatformExpertDevice")), keyname, None, 0) 49 | 50 | 51 | def io_key_string_value(keyname): 52 | """Converts NSData/CFData return value to an NSString""" 53 | raw_value = io_key(keyname) 54 | return NSString.alloc().initWithData_encoding_( 55 | raw_value, NSUTF8StringEncoding 56 | ).rstrip('\0') 57 | 58 | 59 | def sysctl(name, output_type=str): 60 | '''Wrapper for sysctl so we don't have to use subprocess''' 61 | if isinstance(name, str): 62 | name = name.encode('utf-8') 63 | size = c_uint(0) 64 | # Find out how big our buffer will be 65 | libc.sysctlbyname(name, None, byref(size), None, 0) 66 | # Make the buffer 67 | buf = create_string_buffer(size.value) 68 | # Re-run, but provide the buffer 69 | libc.sysctlbyname(name, buf, byref(size), None, 0) 70 | if output_type in (str, 'str'): 71 | return buf.value.decode('UTF-8') 72 | if output_type in (int, 'int'): 73 | # complex stuff to cast the buffer contents to a Python int 74 | if size.value == 4: 75 | return cast(buf, POINTER(c_int32)).contents.value 76 | if size.value == 8: 77 | return cast(buf, POINTER(c_int64)).contents.value 78 | if output_type == 'raw': 79 | # sysctl can also return a 'struct' type; just return the raw buffer 80 | return buf.raw 81 | 82 | 83 | def is_virtual_machine(): 84 | '''Returns True if this is a VM, False otherwise''' 85 | cpu_features = sysctl('machdep.cpu.features').split() 86 | return 'VMM' in cpu_features 87 | 88 | 89 | def is_supported_model(): 90 | '''Returns True if model is in list of supported models, 91 | False otherwise''' 92 | supported_models = [ 93 | u'MacBook10,1', 94 | u'MacBook8,1', 95 | u'MacBook9,1', 96 | u'MacBookAir6,1', 97 | u'MacBookAir6,2', 98 | u'MacBookAir7,1', 99 | u'MacBookAir7,2', 100 | u'MacBookAir8,1', 101 | u'MacBookAir8,2', 102 | u'MacBookPro11,2', 103 | u'MacBookPro11,3', 104 | u'MacBookPro11,4', 105 | u'MacBookPro11,5', 106 | u'MacBookPro12,1', 107 | u'MacBookPro13,1', 108 | u'MacBookPro13,2', 109 | u'MacBookPro13,3', 110 | u'MacBookPro14,1', 111 | u'MacBookPro14,2', 112 | u'MacBookPro14,3', 113 | u'MacBookPro15,1', 114 | u'MacBookPro15,2', 115 | u'MacBookPro15,3', 116 | u'MacBookPro15,4', 117 | u'MacPro6,1', 118 | u'MacPro7,1', 119 | u'Macmini7,1', 120 | u'Macmini8,1', 121 | u'iMac14,4', 122 | u'iMac15,1', 123 | u'iMac16,1', 124 | u'iMac16,2', 125 | u'iMac17,1', 126 | u'iMac18,1', 127 | u'iMac18,2', 128 | u'iMac18,3', 129 | u'iMac19,1', 130 | u'iMac19,2', 131 | u'iMacPro1,1' 132 | ] 133 | current_model = get_current_model() 134 | if not current_model: 135 | return False 136 | elif current_model in supported_models: 137 | return True 138 | else: 139 | return False 140 | 141 | 142 | def get_minor_system_version(): 143 | '''Returns 7 for Lion, 8 for Mountain Lion, etc''' 144 | darwin_version = int(os.uname()[2].split('.')[0]) 145 | return darwin_version - 4 146 | 147 | 148 | def is_supported_system_version(): 149 | '''Returns True if current macOS version is 10.9 through 10.15, 150 | False otherwise''' 151 | macos_minor_version = get_minor_system_version() 152 | if macos_minor_version >= 16: 153 | return False 154 | elif macos_minor_version >= 9: 155 | return True 156 | else: 157 | return False 158 | 159 | 160 | def get_board_id(): 161 | '''Returns our board-id''' 162 | return io_key_string_value("board-id") 163 | 164 | 165 | def get_current_model(): 166 | '''Returns model info''' 167 | return io_key_string_value("model") 168 | 169 | 170 | def is_supported_board_id(): 171 | '''Returns True if current board_id is in list of supported board_ids, 172 | False otherwise''' 173 | platform_support_values = ( 174 | u'Mac-226CB3C6A851A671', 175 | u'Mac-36B6B6DA9CFCD881', 176 | u'Mac-112818653D3AABFC', 177 | u'Mac-9394BDF4BF862EE7', 178 | u'Mac-AA95B1DDAB278B95', 179 | u'Mac-CAD6701F7CEA0921', 180 | u'Mac-50619A408DB004DA', 181 | u'Mac-7BA5B2D9E42DDD94', 182 | u'Mac-CFF7D910A743CAAF', 183 | u'Mac-B809C3757DA9BB8D', 184 | u'Mac-F305150B0C7DEEEF', 185 | u'Mac-35C1E88140C3E6CF', 186 | u'Mac-827FAC58A8FDFA22', 187 | u'Mac-6FEBD60817C77D8A', 188 | u'Mac-7BA5B2DFE22DDD8C', 189 | u'Mac-827FB448E656EC26', 190 | u'Mac-66E35819EE2D0D05', 191 | u'Mac-BE0E8AC46FE800CC', 192 | u'Mac-5A49A77366F81C72', 193 | u'Mac-63001698E7A34814', 194 | u'Mac-937CB26E2E02BB01', 195 | u'Mac-FFE5EF870D7BA81A', 196 | u'Mac-87DCB00F4AD77EEA', 197 | u'Mac-A61BADE1FDAD7B05', 198 | u'Mac-C6F71043CEAA02A6', 199 | u'Mac-4B682C642B45593E', 200 | u'Mac-1E7E29AD0135F9BC', 201 | u'Mac-90BE64C3CB5A9AEB', 202 | u'Mac-3CBD00234E554E41', 203 | u'Mac-B4831CEBD52A0C4C', 204 | u'Mac-E1008331FDC96864', 205 | u'Mac-FA842E06C61E91C5', 206 | u'Mac-81E3E92DD6088272', 207 | u'Mac-06F11FD93F0323C5', 208 | u'Mac-06F11F11946D27C5', 209 | u'Mac-F60DEB81FF30ACF6', 210 | u'Mac-473D31EABEB93F9B', 211 | u'Mac-0CFF9C7C2B63DF8D', 212 | u'Mac-9F18E312C5C2BF0B', 213 | u'Mac-E7203C0F68AA0004', 214 | u'Mac-65CE76090165799A', 215 | u'Mac-CF21D135A7D34AA6', 216 | u'Mac-112B0A653D3AAB9C', 217 | u'Mac-DB15BD556843C820', 218 | u'Mac-27AD2F918AE68F61', 219 | u'Mac-937A206F2EE63C01', 220 | u'Mac-77F17D7DA9285301', 221 | u'Mac-9AE82516C7C6B903', 222 | u'Mac-BE088AF8C5EB4FA2', 223 | u'Mac-551B86E5744E2388', 224 | u'Mac-564FBA6031E5946A', 225 | u'Mac-A5C67F76ED83108C', 226 | u'Mac-5F9802EFE386AA28', 227 | u'Mac-747B1AEFF11738BE', 228 | u'Mac-AF89B6D9451A490B', 229 | u'Mac-EE2EBD4B90B839A8', 230 | u'Mac-42FD25EABCABB274', 231 | u'Mac-2BD1B31983FE1663', 232 | u'Mac-7DF21CB3ED6977E5', 233 | u'Mac-A369DDC4E67F1C45', 234 | u'Mac-35C5E08120C7EEAF', 235 | u'Mac-E43C1C25D4880AD6', 236 | u'Mac-53FDB3D8DB8CA971' 237 | ) 238 | board_id = get_board_id() 239 | return board_id in platform_support_values 240 | 241 | 242 | def fact(): 243 | '''Return our bigsur_upgrade_supported fact''' 244 | if is_virtual_machine(): 245 | return {'bigsur_upgrade_supported': True} 246 | if ((is_supported_model() or is_supported_board_id()) and 247 | is_supported_system_version()): 248 | return {'bigsur_upgrade_supported': True} 249 | return {'bigsur_upgrade_supported': False} 250 | 251 | 252 | if __name__ == '__main__': 253 | # Debug/testing output when run directly 254 | print('is_virtual_machine: %s' % is_virtual_machine()) 255 | print('get_current_model: %s' % get_current_model()) 256 | print('is_supported_model: %s' % is_supported_model()) 257 | print('get_minor_system_version: %s' % get_minor_system_version()) 258 | print('is_supported_system_version: %s' % is_supported_system_version()) 259 | print('get_board_id: %s' % get_board_id()) 260 | print('is_supported_board_id: %s' % is_supported_board_id()) 261 | print(fact()) 262 | -------------------------------------------------------------------------------- /facts/catalina_upgrade_supported.py: -------------------------------------------------------------------------------- 1 | '''Returns a fact to indicate if this machine can be upgraded to Catalina''' 2 | 3 | # Based on 4 | # https://github.com/hjuutilainen/adminscripts/blob/master/ 5 | # check-10.12-sierra-compatibility.py 6 | 7 | # sysctl function by Michael Lynn 8 | # https://gist.github.com/pudquick/581a71425439f2cf8f09 9 | 10 | # IOKit bindings by Michael Lynn 11 | # https://gist.github.com/pudquick/ 12 | # c7dd1262bd81a32663f0#file-get_platform-py-L22-L23 13 | 14 | 15 | from __future__ import absolute_import, print_function 16 | 17 | from ctypes import CDLL, c_uint, byref, create_string_buffer 18 | from ctypes import cast, POINTER 19 | from ctypes.util import find_library 20 | import os 21 | 22 | import objc 23 | 24 | from Foundation import NSBundle, NSString, NSUTF8StringEncoding 25 | 26 | # glue to call C and Cocoa stuff 27 | libc = CDLL(find_library('c')) 28 | IOKit_bundle = NSBundle.bundleWithIdentifier_('com.apple.framework.IOKit') 29 | 30 | functions = [("IOServiceGetMatchingService", b"II@"), 31 | ("IOServiceMatching", b"@*"), 32 | ("IORegistryEntryCreateCFProperty", b"@I@@I"), 33 | ] 34 | 35 | objc.loadBundleFunctions(IOKit_bundle, globals(), functions) 36 | 37 | 38 | def io_key(keyname): 39 | """Gets a raw value from the IORegistry""" 40 | return IORegistryEntryCreateCFProperty( 41 | IOServiceGetMatchingService( 42 | 0, IOServiceMatching(b"IOPlatformExpertDevice")), keyname, None, 0) 43 | 44 | 45 | def io_key_string_value(keyname): 46 | """Converts NSData/CFData return value to an NSString""" 47 | raw_value = io_key(keyname) 48 | return NSString.alloc().initWithData_encoding_( 49 | raw_value, NSUTF8StringEncoding 50 | ).rstrip('\0') 51 | 52 | 53 | def sysctl(name, output_type=str): 54 | '''Wrapper for sysctl so we don't have to use subprocess''' 55 | if isinstance(name, str): 56 | name = name.encode('utf-8') 57 | size = c_uint(0) 58 | # Find out how big our buffer will be 59 | libc.sysctlbyname(name, None, byref(size), None, 0) 60 | # Make the buffer 61 | buf = create_string_buffer(size.value) 62 | # Re-run, but provide the buffer 63 | libc.sysctlbyname(name, buf, byref(size), None, 0) 64 | if output_type in (str, 'str'): 65 | return buf.value.decode('UTF-8') 66 | if output_type in (int, 'int'): 67 | # complex stuff to cast the buffer contents to a Python int 68 | if size.value == 4: 69 | return cast(buf, POINTER(c_int32)).contents.value 70 | if size.value == 8: 71 | return cast(buf, POINTER(c_int64)).contents.value 72 | if output_type == 'raw': 73 | # sysctl can also return a 'struct' type; just return the raw buffer 74 | return buf.raw 75 | 76 | 77 | def is_virtual_machine(): 78 | '''Returns True if this is a VM, False otherwise''' 79 | cpu_features = sysctl('machdep.cpu.features').split() 80 | return 'VMM' in cpu_features 81 | 82 | 83 | def is_supported_model(): 84 | '''Returns False if model is in list of non_supported_models, 85 | True otherwise''' 86 | non_supported_models = [ 87 | 'iMac4,1', 88 | 'iMac4,2', 89 | 'iMac5,1', 90 | 'iMac5,2', 91 | 'iMac6,1', 92 | 'iMac7,1', 93 | 'iMac8,1', 94 | 'iMac9,1', 95 | 'iMac10,1', 96 | 'iMac11,1', 97 | 'iMac11,2', 98 | 'iMac11,3', 99 | 'iMac12,1', 100 | 'iMac12,2', 101 | 'MacBook1,1', 102 | 'MacBook2,1', 103 | 'MacBook3,1', 104 | 'MacBook4,1', 105 | 'MacBook5,1', 106 | 'MacBook5,2', 107 | 'MacBook6,1', 108 | 'MacBook7,1', 109 | 'MacBookAir1,1', 110 | 'MacBookAir2,1', 111 | 'MacBookAir3,1', 112 | 'MacBookAir3,2', 113 | 'MacBookAir4,1', 114 | 'MacBookAir4,2', 115 | 'MacBookPro1,1', 116 | 'MacBookPro1,2', 117 | 'MacBookPro2,1', 118 | 'MacBookPro2,2', 119 | 'MacBookPro3,1', 120 | 'MacBookPro4,1', 121 | 'MacBookPro5,1', 122 | 'MacBookPro5,2', 123 | 'MacBookPro5,3', 124 | 'MacBookPro5,4', 125 | 'MacBookPro5,5', 126 | 'MacBookPro6,1', 127 | 'MacBookPro6,2', 128 | 'MacBookPro7,1', 129 | 'MacBookPro8,1', 130 | 'MacBookPro8,2', 131 | 'MacBookPro8,3', 132 | 'Macmini1,1', 133 | 'Macmini2,1', 134 | 'Macmini3,1', 135 | 'Macmini4,1', 136 | 'Macmini5,1', 137 | 'Macmini5,2', 138 | 'Macmini5,3', 139 | 'MacPro1,1', 140 | 'MacPro2,1', 141 | 'MacPro3,1', 142 | 'MacPro4,1', 143 | 'MacPro5,1', 144 | 'Xserve1,1', 145 | 'Xserve2,1', 146 | 'Xserve3,1', 147 | ] 148 | current_model = get_current_model() 149 | if current_model in non_supported_models: 150 | return False 151 | else: 152 | return True 153 | 154 | 155 | def get_minor_system_version(): 156 | '''Returns 7 for Lion, 8 for Mountain Lion, etc''' 157 | darwin_version = int(os.uname()[2].split('.')[0]) 158 | return darwin_version - 4 159 | 160 | 161 | def is_supported_system_version(): 162 | '''Returns True if current macOS version is 10.9 through 10.14, 163 | False otherwise''' 164 | macos_minor_version = get_minor_system_version() 165 | if macos_minor_version >= 15: 166 | return False 167 | elif macos_minor_version >= 9: 168 | return True 169 | else: 170 | return False 171 | 172 | 173 | def get_board_id(): 174 | '''Returns our board-id''' 175 | return io_key_string_value("board-id") 176 | 177 | 178 | def get_current_model(): 179 | '''Returns model info''' 180 | return io_key_string_value("model") 181 | 182 | 183 | def is_supported_board_id(): 184 | '''Returns True if board_id is in the list of supported values; 185 | False otherwise''' 186 | platform_support_values = [ 187 | 'Mac-00BE6ED71E35EB86', 188 | 'Mac-1E7E29AD0135F9BC', 189 | 'Mac-2BD1B31983FE1663', 190 | 'Mac-2E6FAB96566FE58C', 191 | 'Mac-3CBD00234E554E41', 192 | 'Mac-4B7AC7E43945597E', 193 | 'Mac-4B682C642B45593E', 194 | 'Mac-5A49A77366F81C72', 195 | 'Mac-06F11F11946D27C5', 196 | 'Mac-06F11FD93F0323C5', 197 | 'Mac-6F01561E16C75D06', 198 | 'Mac-7BA5B2D9E42DDD94', 199 | 'Mac-7BA5B2DFE22DDD8C', 200 | 'Mac-7DF2A3B5E5D671ED', 201 | 'Mac-7DF21CB3ED6977E5', 202 | 'Mac-9AE82516C7C6B903', 203 | 'Mac-9F18E312C5C2BF0B', 204 | 'Mac-27AD2F918AE68F61', 205 | 'Mac-27ADBB7B4CEE8E61', 206 | 'Mac-031AEE4D24BFF0B1', 207 | 'Mac-031B6874CF7F642A', 208 | 'Mac-35C1E88140C3E6CF', 209 | 'Mac-35C5E08120C7EEAF', 210 | 'Mac-42FD25EABCABB274', 211 | 'Mac-53FDB3D8DB8CA971', 212 | 'Mac-65CE76090165799A', 213 | 'Mac-66E35819EE2D0D05', 214 | 'Mac-66F35F19FE2A0D05', 215 | 'Mac-77EB7D7DAF985301', 216 | 'Mac-77F17D7DA9285301', 217 | 'Mac-81E3E92DD6088272', 218 | 'Mac-90BE64C3CB5A9AEB', 219 | 'Mac-112B0A653D3AAB9C', 220 | 'Mac-189A3D4F975D5FFC', 221 | 'Mac-226CB3C6A851A671', 222 | 'Mac-473D31EABEB93F9B', 223 | 'Mac-551B86E5744E2388', 224 | 'Mac-747B1AEFF11738BE', 225 | 'Mac-827FAC58A8FDFA22', 226 | 'Mac-827FB448E656EC26', 227 | 'Mac-937A206F2EE63C01', 228 | 'Mac-937CB26E2E02BB01', 229 | 'Mac-9394BDF4BF862EE7', 230 | 'Mac-50619A408DB004DA', 231 | 'Mac-63001698E7A34814', 232 | 'Mac-112818653D3AABFC', 233 | 'Mac-A5C67F76ED83108C', 234 | 'Mac-A369DDC4E67F1C45', 235 | 'Mac-AA95B1DDAB278B95', 236 | 'Mac-AFD8A9D944EA4843', 237 | 'Mac-B809C3757DA9BB8D', 238 | 'Mac-B4831CEBD52A0C4C', 239 | 'Mac-BE0E8AC46FE800CC', 240 | 'Mac-BE088AF8C5EB4FA2', 241 | 'Mac-C3EC7CD22292981F', 242 | 'Mac-C6F71043CEAA02A6', 243 | 'Mac-CAD6701F7CEA0921', 244 | 'Mac-CF21D135A7D34AA6', 245 | 'Mac-DB15BD556843C820', 246 | 'Mac-E43C1C25D4880AD6', 247 | 'Mac-EE2EBD4B90B839A8', 248 | 'Mac-F60DEB81FF30ACF6', 249 | 'Mac-F65AE981FFA204ED', 250 | 'Mac-F305150B0C7DEEEF', 251 | 'Mac-FA842E06C61E91C5', 252 | 'Mac-FC02E91DDD3FA6A4', 253 | 'Mac-FFE5EF870D7BA81A', 254 | ] 255 | board_id = get_board_id() 256 | if board_id in platform_support_values: 257 | return True 258 | else: 259 | return False 260 | 261 | 262 | def fact(): 263 | '''Return our catalina_upgrade_supported fact''' 264 | if is_virtual_machine(): 265 | return {'catalina_upgrade_supported': True} 266 | if (is_supported_model() and is_supported_board_id() and 267 | is_supported_system_version()): 268 | return {'catalina_upgrade_supported': True} 269 | return {'catalina_upgrade_supported': False} 270 | 271 | 272 | if __name__ == '__main__': 273 | # Debug/testing output when run directly 274 | print('is_virtual_machine: %s' % is_virtual_machine()) 275 | print('get_current_model: %s' % get_current_model()) 276 | print('is_supported_model: %s' % is_supported_model()) 277 | print('get_minor_system_version: %s' % get_minor_system_version()) 278 | print('is_supported_system_version: %s' % is_supported_system_version()) 279 | print('get_board_id: %s' % get_board_id()) 280 | print('is_supported_board_id: %s' % is_supported_board_id()) 281 | print(fact()) 282 | -------------------------------------------------------------------------------- /facts/monterey_upgrade_supported.py: -------------------------------------------------------------------------------- 1 | '''Returns a fact to indicate if this machine can be upgraded to macOS 12 Monterey''' 2 | 3 | # Based on 4 | # https://github.com/hjuutilainen/adminscripts/blob/master/ 5 | # check-10.12-sierra-compatibility.py 6 | 7 | # sysctl function by Michael Lynn 8 | # https://gist.github.com/pudquick/581a71425439f2cf8f09 9 | 10 | # IOKit bindings by Michael Lynn 11 | # https://gist.github.com/pudquick/ 12 | # c7dd1262bd81a32663f0#file-get_platform-py-L22-L23 13 | 14 | # Information on what boardIDs and Models that are supported is buried in the installer found here: 15 | # Install macOS Monterey/Contents/SharedSupport/SharedSupport.dmg - mount this 16 | # /Volumes/Shared Support/com_apple_MobileAsset_MacSoftwareUpdate/bc70a04218e8e8bd40d2472aecbb2a06773ba42b.zip - decompress this, the name of the zip will most likely change with every OS update. 17 | # bc70a04218e8e8bd40d2472aecbb2a06773ba42b/AssetData/boot/PlatformSupport.plist 18 | # 19 | # 20 | # device_support_values are harvested from the full installer Distribution file. 21 | # The macOS 12.0.1 Distribution from ProductID 002-23774 was found at http://swcdn.apple.com/content/downloads/39/60/002-23774-A_KNETE2LDIN/4ll6ahj3st7jhqfzzjt1bjp1nhwl4p4zx7/002-23774.English.dist 22 | 23 | from __future__ import absolute_import, print_function 24 | 25 | from ctypes import CDLL, c_uint, byref, create_string_buffer 26 | from ctypes import cast, POINTER 27 | from ctypes.util import find_library 28 | import os 29 | 30 | import objc 31 | 32 | from Foundation import NSBundle, NSString, NSUTF8StringEncoding 33 | 34 | # glue to call C and Cocoa stuff 35 | libc = CDLL(find_library('c')) 36 | IOKit_bundle = NSBundle.bundleWithIdentifier_('com.apple.framework.IOKit') 37 | 38 | functions = [("IOServiceGetMatchingService", b"II@"), 39 | ("IOServiceMatching", b"@*"), 40 | ("IORegistryEntryCreateCFProperty", b"@I@@I"), 41 | ] 42 | 43 | objc.loadBundleFunctions(IOKit_bundle, globals(), functions) 44 | 45 | 46 | def io_key(keyname): 47 | """Gets a raw value from the IORegistry""" 48 | return IORegistryEntryCreateCFProperty( 49 | IOServiceGetMatchingService( 50 | 0, IOServiceMatching(b"IOPlatformExpertDevice")), keyname, None, 0) 51 | 52 | 53 | def io_key_string_value(keyname): 54 | """Converts NSData/CFData return value to an NSString""" 55 | raw_value = io_key(keyname) 56 | return NSString.alloc().initWithData_encoding_( 57 | raw_value, NSUTF8StringEncoding 58 | ).rstrip('\0') 59 | 60 | 61 | def sysctl(name, output_type=str): 62 | '''Wrapper for sysctl so we don't have to use subprocess''' 63 | if isinstance(name, str): 64 | name = name.encode('utf-8') 65 | size = c_uint(0) 66 | # Find out how big our buffer will be 67 | libc.sysctlbyname(name, None, byref(size), None, 0) 68 | # Make the buffer 69 | buf = create_string_buffer(size.value) 70 | # Re-run, but provide the buffer 71 | libc.sysctlbyname(name, buf, byref(size), None, 0) 72 | if output_type in (str, 'str'): 73 | return buf.value.decode('UTF-8') 74 | if output_type in (int, 'int'): 75 | # complex stuff to cast the buffer contents to a Python int 76 | if size.value == 4: 77 | return cast(buf, POINTER(c_int32)).contents.value 78 | if size.value == 8: 79 | return cast(buf, POINTER(c_int64)).contents.value 80 | if output_type == 'raw': 81 | # sysctl can also return a 'struct' type; just return the raw buffer 82 | return buf.raw 83 | 84 | 85 | def is_virtual_machine(): 86 | '''Returns True if this is a VM, False otherwise''' 87 | cpu_features = sysctl('machdep.cpu.features').split() 88 | return 'VMM' in cpu_features 89 | 90 | 91 | def is_supported_model(): 92 | '''Returns True if model is in list of supported models, 93 | False otherwise''' 94 | supported_models = [ 95 | u'MacBook10,1', 96 | u'MacBook9,1', 97 | u'MacBookAir7,1', 98 | u'MacBookAir7,2', 99 | u'MacBookAir8,1', 100 | u'MacBookAir8,2', 101 | u'MacBookAir9,1', 102 | u'MacBookPro11,4', 103 | u'MacBookPro11,5', 104 | u'MacBookPro12,1', 105 | u'MacBookPro13,1', 106 | u'MacBookPro13,2', 107 | u'MacBookPro13,3', 108 | u'MacBookPro14,1', 109 | u'MacBookPro14,2', 110 | u'MacBookPro14,3', 111 | u'MacBookPro15,1', 112 | u'MacBookPro15,2', 113 | u'MacBookPro15,3', 114 | u'MacBookPro15,4', 115 | u'MacBookPro16,1', 116 | u'MacBookPro16,2', 117 | u'MacBookPro16,3', 118 | u'MacBookPro16,4', 119 | u'MacPro6,1', 120 | u'MacPro7,1', 121 | u'Macmini7,1', 122 | u'Macmini8,1', 123 | u'iMac16,1', 124 | u'iMac16,2', 125 | u'iMac17,1', 126 | u'iMac18,1', 127 | u'iMac18,2', 128 | u'iMac18,3', 129 | u'iMac19,1', 130 | u'iMac19,2', 131 | u'iMac20,1', 132 | u'iMac20,2', 133 | u'iMacPro1,1' 134 | ] 135 | current_model = get_current_model() 136 | if not current_model: 137 | return False 138 | elif current_model in supported_models: 139 | return True 140 | else: 141 | return False 142 | 143 | def is_supported_board_id(): 144 | '''Returns True if current board_id is in list of supported board_ids, 145 | False otherwise''' 146 | platform_support_values = ( 147 | u'Mac-06F11F11946D27C5', 148 | u'Mac-06F11FD93F0323C5', 149 | u'Mac-0CFF9C7C2B63DF8D', 150 | u'Mac-112818653D3AABFC', 151 | u'Mac-1E7E29AD0135F9BC', 152 | u'Mac-226CB3C6A851A671', 153 | u'Mac-27AD2F918AE68F61', 154 | u'Mac-35C5E08120C7EEAF', 155 | u'Mac-473D31EABEB93F9B', 156 | u'Mac-4B682C642B45593E', 157 | u'Mac-53FDB3D8DB8CA971', 158 | u'Mac-551B86E5744E2388', 159 | u'Mac-5F9802EFE386AA28', 160 | u'Mac-63001698E7A34814', 161 | u'Mac-65CE76090165799A', 162 | u'Mac-66E35819EE2D0D05', 163 | u'Mac-77F17D7DA9285301', 164 | u'Mac-7BA5B2D9E42DDD94', 165 | u'Mac-7BA5B2DFE22DDD8C', 166 | u'Mac-827FAC58A8FDFA22', 167 | u'Mac-827FB448E656EC26', 168 | u'Mac-937A206F2EE63C01', 169 | u'Mac-937CB26E2E02BB01', 170 | u'Mac-9AE82516C7C6B903', 171 | u'Mac-9F18E312C5C2BF0B', 172 | u'Mac-A369DDC4E67F1C45', 173 | u'Mac-A5C67F76ED83108C', 174 | u'Mac-A61BADE1FDAD7B05', 175 | u'Mac-AA95B1DDAB278B95', 176 | u'Mac-AF89B6D9451A490B', 177 | u'Mac-B4831CEBD52A0C4C', 178 | u'Mac-B809C3757DA9BB8D', 179 | u'Mac-BE088AF8C5EB4FA2', 180 | u'Mac-CAD6701F7CEA0921', 181 | u'Mac-CFF7D910A743CAAF', 182 | u'Mac-DB15BD556843C820', 183 | u'Mac-E1008331FDC96864', 184 | u'Mac-E43C1C25D4880AD6', 185 | u'Mac-E7203C0F68AA0004', 186 | u'Mac-EE2EBD4B90B839A8', 187 | u'Mac-F60DEB81FF30ACF6', 188 | u'Mac-FFE5EF870D7BA81A', 189 | u'VMM-x86_64' 190 | ) 191 | board_id = get_board_id() 192 | return board_id in platform_support_values 193 | 194 | def is_supported_device_id(): 195 | '''Returns True if current device_id is in list of supported device_ids, 196 | False otherwise''' 197 | device_support_values = ( 198 | u'J132AP', 199 | u'J137AP', 200 | u'J140AAP', 201 | u'J140KAP', 202 | u'J152FAP', 203 | u'J160AP', 204 | u'J174AP', 205 | u'J185AP', 206 | u'J185FAP', 207 | u'J213AP', 208 | u'J214AP', 209 | u'J214KAP', 210 | u'J215AP', 211 | u'J223AP', 212 | u'J230AP', 213 | u'J230KAP', 214 | u'J274AP', 215 | u'J293AP', 216 | u'J313AP', 217 | u'J314cAP', 218 | u'J314sAP', 219 | u'J316cAP', 220 | u'J316sAP' 221 | u'J456AP', 222 | u'J457AP', 223 | u'J680AP', 224 | u'J780AP', 225 | u'VMA2MACOSAP', 226 | u'VMM-x86_64', 227 | u'X589AMLUAP', 228 | u'X86LEGACYAP' 229 | ) 230 | device_support_values = [deviceid.lower() for deviceid in device_support_values] 231 | device_id = get_device_id() 232 | return device_id in device_support_values 233 | 234 | 235 | def get_minor_system_version(): 236 | '''Returns 20 for Big Sur, 21 for Monterey, etc''' 237 | darwin_version = int(os.uname()[2].split('.')[0]) 238 | return darwin_version - 4 239 | 240 | 241 | def is_supported_system_version(): 242 | '''Returns True if current macOS version is 10.9 through 13.x, 243 | False otherwise''' 244 | macos_minor_version = get_minor_system_version() 245 | if macos_minor_version >= 17: 246 | return False 247 | elif macos_minor_version >= 9: 248 | return True 249 | else: 250 | return False 251 | 252 | def get_board_id(): 253 | '''Returns our board-id''' 254 | return io_key_string_value("board-id") 255 | 256 | def get_device_id(): 257 | '''Returns our device-id''' 258 | deviceid = sysctl("hw.target") 259 | return deviceid.lower() 260 | 261 | def get_current_model(): 262 | '''Returns model info''' 263 | return io_key_string_value("model") 264 | 265 | def fact(): 266 | '''Return our monterey_upgrade_supported fact''' 267 | if is_virtual_machine(): 268 | return {'monterey_upgrade_supported': True} 269 | if ((is_supported_model() or is_supported_board_id() or is_supported_device_id()) and 270 | is_supported_system_version()): 271 | return {'monterey_upgrade_supported': True} 272 | return {'monterey_upgrade_supported': False} 273 | 274 | 275 | if __name__ == '__main__': 276 | # Debug/testing output when run directly 277 | print('is_virtual_machine: %s' % is_virtual_machine()) 278 | print('get_current_model: %s' % get_current_model()) 279 | print('is_supported_model: %s' % is_supported_model()) 280 | print('get_minor_system_version: %s' % get_minor_system_version()) 281 | print('is_supported_system_version: %s' % is_supported_system_version()) 282 | print('get_board_id: %s' % get_board_id()) 283 | print('is_supported_board_id: %s' % is_supported_board_id()) 284 | print('get_device_id: %s' % get_device_id()) 285 | print('is_supported_device_id: %s' % is_supported_device_id()) 286 | print(fact()) 287 | -------------------------------------------------------------------------------- /facts/macos_upgrade_supported.py: -------------------------------------------------------------------------------- 1 | '''Returns facts that indicate if this machine can be upgraded to macOS 13 or newer''' 2 | 3 | # Based on 4 | # https://github.com/hjuutilainen/adminscripts/blob/master/ 5 | # check-10.12-sierra-compatibility.py 6 | 7 | # sysctl function by Michael Lynn 8 | # https://gist.github.com/pudquick/581a71425439f2cf8f09 9 | 10 | # IOKit bindings by Michael Lynn 11 | # https://gist.github.com/pudquick/ 12 | # c7dd1262bd81a32663f0#file-get_platform-py-L22-L23 13 | 14 | # Information on supported models is buried in the installer found here: 15 | # Install macOS Sequoia.app/Contents/SharedSupport/SharedSupport.dmg - mount this 16 | # /Volumes/Shared Support/com_apple_MobileAsset_MacSoftwareUpdate/LONG_HEX_STRING.zip 17 | # - decompress this, the name of the zip will most likely change with every OS update. 18 | # - Combining, sorting, and de-duping values from the following result in a list of supported models 19 | # - 'SupportedProductTypes' from LONG_HEX_STRING/AssetData/boot/Restore.plist 20 | # - 'SupportedModelProperties' from LONG_HEX_STRING/AssetData/boot/PlatformSupport.plist 21 | 22 | 23 | MACOS_RELEASES = [ 24 | { 25 | "name": "tahoe", 26 | "version": 26, 27 | "supported_models": [ 28 | 'iMac20,1', 29 | 'iMac20,2', 30 | 'iMac21,1', 31 | 'iMac21,2', 32 | 'Mac13,1', 33 | 'Mac13,2', 34 | 'Mac14,10', 35 | 'Mac14,12', 36 | 'Mac14,13', 37 | 'Mac14,14', 38 | 'Mac14,15', 39 | 'Mac14,2', 40 | 'Mac14,3', 41 | 'Mac14,5', 42 | 'Mac14,6', 43 | 'Mac14,7', 44 | 'Mac14,8', 45 | 'Mac14,9', 46 | 'Mac15,10', 47 | 'Mac15,11', 48 | 'Mac15,12', 49 | 'Mac15,13', 50 | 'Mac15,14', 51 | 'Mac15,3', 52 | 'Mac15,4', 53 | 'Mac15,5', 54 | 'Mac15,6', 55 | 'Mac15,7', 56 | 'Mac15,8', 57 | 'Mac15,9', 58 | 'Mac16,1', 59 | 'Mac16,2', 60 | 'Mac16,3', 61 | 'Mac16,5', 62 | 'Mac16,6', 63 | 'Mac16,7', 64 | 'Mac16,8', 65 | 'Mac16,9', 66 | 'Mac16,10', 67 | 'Mac16,11', 68 | 'Mac16,12', 69 | 'Mac16,13', 70 | 'MacBookAir10,1', 71 | 'MacBookPro16,1', 72 | 'MacBookPro16,2', 73 | 'MacBookPro16,4', 74 | 'MacBookPro17,1', 75 | 'MacBookPro18,1', 76 | 'MacBookPro18,2', 77 | 'MacBookPro18,3', 78 | 'MacBookPro18,4', 79 | 'Macmini9,1', 80 | 'MacPro7,1', 81 | 'VirtualMac2,1', 82 | ] 83 | }, 84 | { 85 | "name": "sequoia", 86 | "version": 15, 87 | "supported_models": [ 88 | 'iMac19,1', 89 | 'iMac19,2', 90 | 'iMac20,1', 91 | 'iMac20,2', 92 | 'iMac21,1', 93 | 'iMac21,2', 94 | 'iMacPro1,1', 95 | 'Mac13,1', 96 | 'Mac13,2', 97 | 'Mac14,10', 98 | 'Mac14,12', 99 | 'Mac14,13', 100 | 'Mac14,14', 101 | 'Mac14,15', 102 | 'Mac14,2', 103 | 'Mac14,3', 104 | 'Mac14,5', 105 | 'Mac14,6', 106 | 'Mac14,7', 107 | 'Mac14,8', 108 | 'Mac14,9', 109 | 'Mac15,10', 110 | 'Mac15,11', 111 | 'Mac15,12', 112 | 'Mac15,13', 113 | 'Mac15,3', 114 | 'Mac15,4', 115 | 'Mac15,5', 116 | 'Mac15,6', 117 | 'Mac15,7', 118 | 'Mac15,8', 119 | 'Mac15,9', 120 | 'MacBookAir10,1', 121 | 'MacBookAir9,1', 122 | 'MacBookPro15,1', 123 | 'MacBookPro15,2', 124 | 'MacBookPro15,3', 125 | 'MacBookPro15,4', 126 | 'MacBookPro16,1', 127 | 'MacBookPro16,2', 128 | 'MacBookPro16,3', 129 | 'MacBookPro16,4', 130 | 'MacBookPro17,1', 131 | 'MacBookPro18,1', 132 | 'MacBookPro18,2', 133 | 'MacBookPro18,3', 134 | 'MacBookPro18,4', 135 | 'Macmini8,1', 136 | 'Macmini9,1', 137 | 'MacPro7,1', 138 | 'VirtualMac2,1', 139 | ] 140 | }, 141 | { 142 | "name": "sonoma", 143 | "version": 14, 144 | "supported_models": [ 145 | 'iMac19,1', 146 | 'iMac19,2', 147 | 'iMac20,1', 148 | 'iMac20,2', 149 | 'iMac21,1', 150 | 'iMac21,2', 151 | 'iMacPro1,1', 152 | 'iSim1,1', 153 | 'Mac13,1', 154 | 'Mac13,2', 155 | 'Mac14,10', 156 | 'Mac14,12', 157 | 'Mac14,13', 158 | 'Mac14,14', 159 | 'Mac14,15', 160 | 'Mac14,2', 161 | 'Mac14,3', 162 | 'Mac14,5', 163 | 'Mac14,6', 164 | 'Mac14,7', 165 | 'Mac14,8', 166 | 'Mac14,9', 167 | 'Mac15,3', 168 | 'Mac15,4', 169 | 'Mac15,5', 170 | 'Mac15,6', 171 | 'Mac15,7', 172 | 'Mac15,8', 173 | 'Mac15,9', 174 | 'MacBookAir10,1', 175 | 'MacBookAir8,1', 176 | 'MacBookAir8,2', 177 | 'MacBookAir9,1', 178 | 'MacBookPro15,1', 179 | 'MacBookPro15,2', 180 | 'MacBookPro15,3', 181 | 'MacBookPro15,4', 182 | 'MacBookPro16,1', 183 | 'MacBookPro16,2', 184 | 'MacBookPro16,3', 185 | 'MacBookPro16,4', 186 | 'MacBookPro17,1', 187 | 'MacBookPro18,1', 188 | 'MacBookPro18,2', 189 | 'MacBookPro18,3', 190 | 'MacBookPro18,4', 191 | 'Macmini8,1', 192 | 'Macmini9,1', 193 | 'MacPro7,1', 194 | 'VirtualMac2,1' 195 | ] 196 | }, 197 | { 198 | "name": "ventura", 199 | "version": 13, 200 | "supported_models": [ 201 | 'iMac18,1', 202 | 'iMac18,2', 203 | 'iMac18,3', 204 | 'iMac19,1', 205 | 'iMac19,2', 206 | 'iMac20,1', 207 | 'iMac20,2', 208 | 'iMac21,1', 209 | 'iMac21,2', 210 | 'iMacPro1,1', 211 | 'iSim1,1', 212 | 'Mac13,1', 213 | 'Mac13,2', 214 | 'Mac14,2', 215 | 'Mac14,7', 216 | 'MacBook10,1', 217 | 'MacBookAir10,1', 218 | 'MacBookAir8,1', 219 | 'MacBookAir8,2', 220 | 'MacBookAir9,1', 221 | 'MacBookPro14,1', 222 | 'MacBookPro14,2', 223 | 'MacBookPro14,3', 224 | 'MacBookPro15,1', 225 | 'MacBookPro15,2', 226 | 'MacBookPro15,3', 227 | 'MacBookPro15,4', 228 | 'MacBookPro16,1', 229 | 'MacBookPro16,2', 230 | 'MacBookPro16,3', 231 | 'MacBookPro16,4', 232 | 'MacBookPro17,1', 233 | 'MacBookPro18,1', 234 | 'MacBookPro18,2', 235 | 'MacBookPro18,3', 236 | 'MacBookPro18,4', 237 | 'Macmini8,1', 238 | 'Macmini9,1', 239 | 'MacPro7,1', 240 | 'VirtualMac2,1' 241 | ] 242 | }, 243 | ] 244 | 245 | 246 | import os 247 | import platform 248 | 249 | from ctypes import CDLL, c_uint, byref, create_string_buffer 250 | from ctypes import cast, POINTER, c_int32, c_int64 251 | from ctypes.util import find_library 252 | 253 | import objc 254 | 255 | from Foundation import NSBundle, NSString, NSUTF8StringEncoding 256 | 257 | # glue to call C and Cocoa stuff 258 | libc = CDLL(find_library('c')) 259 | IOKit_bundle = NSBundle.bundleWithIdentifier_('com.apple.framework.IOKit') 260 | 261 | functions = [("IOServiceGetMatchingService", b"II@"), 262 | ("IOServiceMatching", b"@*"), 263 | ("IORegistryEntryCreateCFProperty", b"@I@@I"), 264 | ] 265 | 266 | objc.loadBundleFunctions(IOKit_bundle, globals(), functions) 267 | 268 | 269 | def io_key(keyname): 270 | """Gets a raw value from the IORegistry""" 271 | return IORegistryEntryCreateCFProperty( 272 | IOServiceGetMatchingService( 273 | 0, IOServiceMatching(b"IOPlatformExpertDevice")), keyname, None, 0) 274 | 275 | 276 | def io_key_string_value(keyname): 277 | """Converts NSData/CFData return value to an NSString""" 278 | raw_value = io_key(keyname) 279 | return NSString.alloc().initWithData_encoding_( 280 | raw_value, NSUTF8StringEncoding 281 | ).rstrip('\0') 282 | 283 | 284 | def sysctl(name, output_type=str): 285 | '''Wrapper for sysctl so we don't have to use subprocess''' 286 | if isinstance(name, str): 287 | name = name.encode('utf-8') 288 | size = c_uint(0) 289 | # Find out how big our buffer will be 290 | libc.sysctlbyname(name, None, byref(size), None, 0) 291 | # Make the buffer 292 | buf = create_string_buffer(size.value) 293 | # Re-run, but provide the buffer 294 | libc.sysctlbyname(name, buf, byref(size), None, 0) 295 | if output_type in (str, 'str'): 296 | return buf.value.decode('UTF-8') 297 | if output_type in (int, 'int'): 298 | # complex stuff to cast the buffer contents to a Python int 299 | if size.value == 4: 300 | return cast(buf, POINTER(c_int32)).contents.value 301 | if size.value == 8: 302 | return cast(buf, POINTER(c_int64)).contents.value 303 | if output_type == 'raw': 304 | # sysctl can also return a 'struct' type; just return the raw buffer 305 | return buf.raw 306 | 307 | 308 | def get_macos_version(): 309 | return int(platform.mac_ver()[0].split('.')[0]) 310 | 311 | 312 | def is_virtual_machine(): 313 | '''Returns True if this is a VM, False otherwise''' 314 | if get_macos_version() >= 11: 315 | hv_vmm_present = sysctl('kern.hv_vmm_present', output_type=int) 316 | return bool(hv_vmm_present) 317 | else: 318 | cpu_features = sysctl('machdep.cpu.features').split() 319 | return 'VMM' in cpu_features 320 | 321 | 322 | def get_current_model(): 323 | '''Returns model info''' 324 | return io_key_string_value("model") 325 | 326 | 327 | def is_supported_model(supported_models): 328 | '''Returns True if model is in list of supported models, 329 | False otherwise''' 330 | return get_current_model() in supported_models 331 | 332 | 333 | def fact(): 334 | '''Return a fact for each os''' 335 | facts = {} 336 | for release in MACOS_RELEASES: 337 | fact_name = release["name"] + '_upgrade_supported' 338 | if is_virtual_machine(): 339 | facts[fact_name] = True 340 | elif ((is_supported_model(release["supported_models"])) and 341 | get_macos_version() < release["version"]): 342 | facts[fact_name] = True 343 | else: 344 | facts[fact_name] = False 345 | return facts 346 | 347 | 348 | if __name__ == '__main__': 349 | # Debug/testing output when run directly 350 | print('is_virtual_machine:\t\t%s' % is_virtual_machine()) 351 | print('get_current_model:\t\t%s' % get_current_model()) 352 | print('get_macos_version:\t\t%s' % get_macos_version()) 353 | for k, v in fact().items(): 354 | print(f'{k}:\t{v}') 355 | --------------------------------------------------------------------------------