Merge branch 'munki:main' into main

This commit is contained in:
Graham Pugh 2021-09-17 21:24:07 +02:00 committed by GitHub
commit c313531939
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 233 additions and 0 deletions

View File

@ -3,6 +3,12 @@
Some scripts that might be of use to macOS admins. Might be related to Munki;
might not.
These are only supported using Apple's Python on macOS. There is no support for running these on Windows or Linux.
#### getmacosipsws.py
Quick-and-dirty tool to download the macOS IPSW files currently advertised by Apple in the https://mesu.apple.com/assets/macos/com_apple_macOSIPSW/com_apple_macOSIPSW.xml feed.
#### installinstallmacos.py
This script can create disk images containing macOS Installer applications available via Apple's softwareupdate catalogs.

227
getmacosipsws.py Executable file
View File

@ -0,0 +1,227 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2021 Greg Neagle.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''Parses Apple's feed of macOS IPSWs and lets you download one'''
from __future__ import (
absolute_import, division, print_function, unicode_literals)
import os
import plistlib
import subprocess
import sys
try:
# python 2
from urllib.parse import urlsplit
except ImportError:
# python 3
from urlparse import urlsplit
from xml.parsers.expat import ExpatError
class ReplicationError(Exception):
'''A custom error when replication fails'''
pass
def get_url(url,
download_dir='/tmp',
show_progress=False,
attempt_resume=False):
'''Downloads a URL and stores it in the download_dir.
Returns a path to the replicated file.'''
path = urlsplit(url)[2]
filename = os.path.basename(path)
local_file_path = os.path.join(download_dir, filename)
if show_progress:
options = '-fL'
else:
options = '-sfL'
need_download = True
while need_download:
curl_cmd = ['/usr/bin/curl', options,
'--create-dirs',
'-o', local_file_path,
'-w', '%{http_code}']
if not url.endswith(".gz"):
# stupid hack for stupid Apple behavior where it sometimes returns
# compressed files even when not asked for
curl_cmd.append('--compressed')
resumed = False
if os.path.exists(local_file_path):
if not attempt_resume:
curl_cmd.extend(['-z', local_file_path])
else:
resumed = True
curl_cmd.extend(['-z', '-' + local_file_path, '-C', '-'])
curl_cmd.append(url)
print("Downloading %s..." % url)
need_download = False
try:
_ = subprocess.check_output(curl_cmd)
except subprocess.CalledProcessError as err:
if not resumed or not err.output.isdigit():
raise ReplicationError(err)
# HTTP error 416 on resume: the download is already complete and the
# file is up-to-date
# HTTP error 412 on resume: the file was updated server-side
if int(err.output) == 412:
print("Removing %s and retrying." % local_file_path)
os.unlink(local_file_path)
need_download = True
elif int(err.output) != 416:
raise ReplicationError(err)
return local_file_path
def get_input(prompt=None):
'''Python 2 and 3 wrapper for raw_input/input'''
try:
return raw_input(prompt)
except NameError:
# raw_input doesn't exist in Python 3
return input(prompt)
def read_plist(filepath):
'''Wrapper for the differences between Python 2 and Python 3's plistlib'''
try:
with open(filepath, "rb") as fileobj:
return plistlib.load(fileobj)
except AttributeError:
# plistlib module doesn't have a load function (as in Python 2)
return plistlib.readPlist(filepath)
def read_plist_from_string(bytestring):
'''Wrapper for the differences between Python 2 and Python 3's plistlib'''
try:
return plistlib.loads(bytestring)
except AttributeError:
# plistlib module doesn't have a load function (as in Python 2)
return plistlib.readPlistFromString(bytestring)
IPSW_DATA = None
def get_ipsw_data():
'''Return data from com_apple_macOSIPSW.xml (which is actually a plist)'''
global IPSW_DATA
IPSW_FEED = "https://mesu.apple.com/assets/macos/com_apple_macOSIPSW/com_apple_macOSIPSW.xml"
if not IPSW_DATA:
try:
ipsw_plist = get_url(IPSW_FEED)
IPSW_DATA = read_plist(ipsw_plist)
except (OSError, IOError, ExpatError, ReplicationError) as err:
print(err, file=sys.stderr)
exit(1)
return IPSW_DATA
def getMobileDeviceSoftwareVersionsByVersion():
'''return the MobileDeviceSoftwareVersionsByVersion dict'''
ipsw_data = get_ipsw_data()
return ipsw_data.get("MobileDeviceSoftwareVersionsByVersion", {})
def getMobileDeviceSoftwareVersions(version=1):
'''Return the dict under the version number key. Current xml has only "1"'''
return getMobileDeviceSoftwareVersionsByVersion().get("%s" % version, {})
def getMachineModelsForMobileDeviceSoftwareVersions(version=1):
'''Get the model keys'''
versions = getMobileDeviceSoftwareVersions(version=version).get(
"MobileDeviceSoftwareVersions", {})
return versions.keys()
def getSoftwareVersionsForMachineModel(model, version=1):
'''Get the dict for a specific model'''
versions = getMobileDeviceSoftwareVersions(version=version).get(
"MobileDeviceSoftwareVersions", {})
return versions[model]
def getIPSWInfoForMachineModel(model, version=1):
'''Build and return a list of dict describing the available
ipsw file for a specific model'''
model_info_list = []
model_versions = getSoftwareVersionsForMachineModel(model, version=version)
for key in model_versions:
if key == "Unknown":
build_dict = model_versions["Unknown"].get("Universal", {})
else:
build_dict = model_versions[key]
restore_info = build_dict.get("Restore")
if restore_info:
model_info = {"model": model}
model_info.update(restore_info)
model_info_list.append(model_info)
return model_info_list
def getAllModelInfo(version=1):
'''Build and return a list of all available ipsws'''
all_model_info = []
available_models = getMachineModelsForMobileDeviceSoftwareVersions(
version=version)
for model in available_models:
model_info = getIPSWInfoForMachineModel(model, version=version)
all_model_info.extend(model_info)
return all_model_info
def main():
'''Our main thing to do'''
all_model_info = getAllModelInfo()
# display a menu of choices
print('%2s %16s %10s %8s %11s'
% ('#', 'Model', 'Version', 'Build', 'Checksum'))
for index, item in enumerate(all_model_info):
print('%2s %16s %10s %8s %11s' % (
index + 1,
item["model"],
item.get('ProductVersion', 'UNKNOWN'),
item.get('BuildVersion', 'UNKNOWN'),
item.get('FirmwareSHA1', 'UNKNOWN')[-6:]))
answer = get_input(
'\nChoose a product to download (1-%s): ' % len(all_model_info))
try:
index = int(answer) - 1
if index < 0:
raise ValueError
except (ValueError, IndexError):
print('Exiting.')
exit(0)
download_url = getAllModelInfo()[index].get("FirmwareURL")
if download_url:
try:
filepath = get_url(download_url,
download_dir=".", show_progress=True, attempt_resume=True)
print("IPSW downloaded to: %s" % filepath)
except (ReplicationError, IOError, OSError) as err:
print(err, file=sys.stderr)
exit(1)
else:
print("No valid download URL for that item.", file=sys.stderr)
exit(1)
if __name__ == '__main__':
main()