mirror of
https://github.com/MLBZ521/MacAdmin.git
synced 2026-02-03 05:53:26 +00:00
272 lines
12 KiB
Python
272 lines
12 KiB
Python
#!/usr/bin/python
|
|
"""
|
|
###################################################################################################
|
|
# Script Name: Apple_RepairPrograms.py
|
|
# By: Zack Thompson / Created: 8/24/2019
|
|
# Version: 1.0.0 / Updated: 8/24/2019 / By: ZT
|
|
#
|
|
# Description: This script looks up provided devices and checks if they're eligible for a recall program.
|
|
#
|
|
# Exchange and Repair Extension Programs: https://support.apple.com/exchange_repair
|
|
#
|
|
# Inspired by the following thread on Jamf Nation: https://www.jamf.com/jamf-nation/discussions/32400/battery-recall-for-15-mid-2015-mbp
|
|
# And the various forked scripts shared by:
|
|
# Nick Tong (https://www.jamf.com/jamf-nation/discussions/32400/battery-recall-for-15-mid-2015-mbp#responseChild186454)
|
|
# Neil Martin (https://soundmacguy.wordpress.com/2019/06/21/2015-macbook-pro-battery-recall-checker-script/)
|
|
# William McGrath (https://gist.github.com/nzmacgeek/ef453c8e96a67ee12d973e4c0594e286)
|
|
# jsherwood (https://www.jamf.com/jamf-nation/discussions/32400/battery-recall-for-15-mid-2015-mbp#responseChild186536)
|
|
#
|
|
###################################################################################################
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import uuid
|
|
|
|
try:
|
|
from urllib import request as urllib # For Python 3
|
|
except ImportError:
|
|
import urllib2 as urllib # For Python 2
|
|
|
|
|
|
def runUtility(command):
|
|
"""A helper function for subprocess.
|
|
Args:
|
|
command: String containing the commands and arguments that will be passed to a shell.
|
|
Returns:
|
|
stdout: output of the command
|
|
"""
|
|
|
|
try:
|
|
process = subprocess.check_output(command, shell=True)
|
|
except subprocess.CalledProcessError as error:
|
|
print ('Error code: {}'.format(error.returncode))
|
|
print ('Error: {}'.format(error))
|
|
process = "error"
|
|
|
|
return process
|
|
|
|
|
|
# This function performs the lookup for each serial number against Apple's API
|
|
def exchange_lookup(program, serial, guid):
|
|
|
|
data = {"serial": serial, "GUID": guid}
|
|
url = "https://qualityprograms.apple.com/snlookup/{program}".format(program=program)
|
|
|
|
try:
|
|
# print('Trying urllib...')
|
|
headers = {'Accept': 'application/json'}
|
|
request = urllib.Request(url, data=json.dumps(data), headers=headers)
|
|
response = urllib.urlopen(request)
|
|
statusCode = response.code
|
|
json_response = json.loads(response.read())
|
|
|
|
except Exception:
|
|
# If urllib fails, resort to using curl
|
|
sys.exc_clear()
|
|
# print('Trying curl...')
|
|
# Build the command
|
|
curl_cmd = '/usr/bin/curl --silent --show-error --no-buffer --fail --write-out "statusCode:%{{http_code}}" --location --header "Accept: application/json" --data "{data}" --url {url} --request GET'.format(data=data, url=url)
|
|
response = runUtility(curl_cmd)
|
|
json_content, statusCode = response.split('statusCode:')
|
|
json_response = json.loads(json_content)
|
|
|
|
return 200, 'json_response'
|
|
|
|
|
|
# This function checks if the model has an available exchange program
|
|
def available_exchange_programs(model):
|
|
program_number = []
|
|
|
|
mbp15Battery = re.compile("(?:MacBook Pro \(Retina, 15-inch, Mid 2015\))|(?:15-inch Retina MacBook Pro \(Mid 2015\))|(?:MacBookPro11,4)|(?:MacBookPro11,5)")
|
|
if mbp15Battery.search(str(model)):
|
|
# 15-inch MacBook Pro Battery Recall Program
|
|
# https://support.apple.com/15-inch-macbook-pro-battery-recall
|
|
program_number.append("062019")
|
|
|
|
mbp13SSD = re.compile("(?:MacBook Pro \(13-inch, 2017, Two Thunderbolt 3 ports\))|(?:13-inch Retina MacBook Pro \(Mid 2017\))|(?:MacBookPro11,4)|(?:MacBookPro14,1)")
|
|
if mbp13SSD.search(str(model)):
|
|
# 13-inch MacBook Pro (non Touch Bar) Solid-State Drive Service Program
|
|
# https://support.apple.com/13-inch-macbook-pro-solid-state-drive-service
|
|
program_number.append("112018")
|
|
|
|
mbp13Battery = re.compile("(?:13-inch MacBook Pro \(non Touch Bar\))|(?:13-inch Retina MacBook Pro \(Late 2016\))|(?:13-inch Retina MacBook Pro \(Mid 2017\))|(?:MacBookPro13,1)|(?:MacBookPro14,1)")
|
|
if mbp13Battery.search(str(model)):
|
|
# 13-inch MacBook Pro (non Touch Bar) Battery Replacement Program
|
|
# https://support.apple.com/13inch-macbookpro-battery-replacement
|
|
program_number.append("032018")
|
|
|
|
if model == "iPhone 8":
|
|
# iPhone 8 Logic Board Replacement Program
|
|
# https://support.apple.com/iphone-8-logic-board-replacement-program
|
|
program_number.append("082018")
|
|
|
|
if model == "iPhone 6S":
|
|
# iPhone 6s Program for Unexpected Shutdown Issues
|
|
# https://support.apple.com/iphone6s-unexpectedshutdown
|
|
program_number.append("112016")
|
|
|
|
if model == "iPhone 6 Plus":
|
|
# iSight Camera Replacement Program for iPhone 6 Plus
|
|
# https://support.apple.com/iphone6plus-isightcamera
|
|
program_number.append("082015")
|
|
|
|
return program_number
|
|
|
|
|
|
# This function takes a serial number and it's model, checks for available exchange programs and the loops through each program ID to check if it's eligible
|
|
def loop(model, serial):
|
|
results = []
|
|
|
|
# Check if model has available exchange program
|
|
programs = available_exchange_programs(model)
|
|
|
|
# If available program(s) is found, loop through each
|
|
if programs:
|
|
for program in programs:
|
|
|
|
# Query Apple's API to see if device is eligible
|
|
status_code, json_data = exchange_lookup(program, serial, str(uuid.uuid1()))
|
|
|
|
# Verify the status code was successful
|
|
if int(status_code) == 200:
|
|
# Get the attributes
|
|
status = json_data["status"]
|
|
|
|
# Check if the device is eligible -- if not, move on without track this
|
|
if status == "E00":
|
|
print("{} is eligible for program: {}".format(serial, program))
|
|
results.append(program)
|
|
|
|
# Sleep so we don't DDoS Apple!
|
|
time.sleep(10)
|
|
|
|
return results
|
|
|
|
|
|
def main():
|
|
|
|
##################################################
|
|
# Define Script Parameters
|
|
|
|
parser = argparse.ArgumentParser(description="Apple Exchange and Repair Extension Programs Lookup")
|
|
single_run = parser.add_argument_group('Single Device')
|
|
batch_run = parser.add_argument_group('Batch Process')
|
|
|
|
single_run.add_argument('--serialnumber', '-s', metavar='C02LA1K9G7DM', type=str, help='A single serial number', required=False)
|
|
single_run.add_argument('--model', '-m', metavar='MacBookPro11,4', type=str, help='A model or model identifier. \
|
|
Example: MacBook Pro (Retina, 15-inch, Mid 2015) or 15-inch Retina MacBook Pro (Mid 2015) or MacBookPro11,4', required=False)
|
|
batch_run.add_argument('--input', '-i', metavar='/path/to/input_file.csv', type=str, help='Path to a CSV with a list of serial numbers, and models or model identifiers. \
|
|
Example: MacBook Pro (Retina, 15-inch, Mid 2015) or 15-inch Retina MacBook Pro (Mid 2015) or MacBookPro11,4', required=False)
|
|
batch_run.add_argument('--output', '-o', metavar='/path/to/output_file.csv', type=str, help='Path to a CSV where devices that are eligible for a repair will be written. \
|
|
WARNING: If the files exists, it will be overwritten!', required=False)
|
|
# parser.add_argument('--quiet', '-q', action='store_true', help='Do not print verbose messages.', required=False)
|
|
|
|
args = parser.parse_args()
|
|
# args = parser.parse_known_args()
|
|
# args = args[0]
|
|
|
|
# print('Argparse args: {}'.format(args))
|
|
|
|
##################################################
|
|
# Bits Staged
|
|
|
|
if len(sys.argv) == 0:
|
|
parser.print_help()
|
|
sys.exit(0)
|
|
else:
|
|
if ( args.input or args.output ) and ( args.serialnumber or args.model ):
|
|
parser.print_help()
|
|
parser.exit(status=1, message='\nError: Unable to mix arguments between parameter groups.\n')
|
|
|
|
# if args.quiet:
|
|
# verbose = False
|
|
# else:
|
|
# verbose = True
|
|
|
|
# If working with a csv...
|
|
if args.input and args.output:
|
|
input_file = args.input
|
|
output_file = args.output
|
|
eligible_devices = []
|
|
serial_fieldname = None
|
|
model_fieldname = None
|
|
|
|
if os.path.exists(input_file):
|
|
# Open the provided CSV file
|
|
with open(input_file, 'r') as csv_in:
|
|
csv_reader = csv.DictReader(csv_in, delimiter=',')
|
|
|
|
# Get the field names so we can parse these for the fields we need as well as use for writing back out later
|
|
field_names = csv_reader.fieldnames
|
|
|
|
# Common, expected patterns that a column header by look like for what is needed
|
|
patternSerial = re.compile("[Ss]erial([\s_])?([Nn]umber)?")
|
|
patternModel = re.compile("[Mm]odel([\s_])?([Ii]dentifier)?")
|
|
|
|
# For each field name, check if one matches
|
|
for field_name in field_names:
|
|
if patternSerial.search(field_name):
|
|
serial_fieldname = field_name
|
|
if patternModel.search(field_name):
|
|
model_fieldname = field_name
|
|
|
|
# Verify the column values were found
|
|
if serial_fieldname == None or model_fieldname == None:
|
|
print("Aborting: Unable to correlate the header columns in the CSV file to expected values.")
|
|
sys.exit(3)
|
|
|
|
# Loop through each row in the CSV file
|
|
for row in csv_reader:
|
|
|
|
# Pass each row attributes to the loop function
|
|
results = loop(model=row[model_fieldname], serial=row[serial_fieldname])
|
|
|
|
# Verify a result was found, if so, add it to a tracking list
|
|
if len(results) != 0:
|
|
row.update({'Eligible Programs': str(results).strip('[]\'')})
|
|
eligible_devices.append(row)
|
|
|
|
# if len(eligible_devices) == 2:
|
|
# break
|
|
|
|
# Check if any devices were eligible before doing anything else
|
|
if len(eligible_devices) > 0:
|
|
|
|
# Write to the CSV specified, if it exists, it will be overwritten
|
|
with open(output_file, mode='w') as csv_out:
|
|
|
|
# Build our new header...
|
|
header_line = field_names
|
|
header_line.append("Eligible Programs")
|
|
writer = csv.DictWriter(csv_out, fieldnames=header_line)
|
|
writer.writeheader()
|
|
|
|
for device in eligible_devices:
|
|
writer.writerow(device)
|
|
else:
|
|
print('None of the devices provided were eligible for a recall program.')
|
|
sys.exit(0)
|
|
|
|
elif args.serialnumber and args.model:
|
|
# A single serial number and model were provided
|
|
input_serialnumber = args.serialnumber
|
|
input_model = args.model
|
|
|
|
# Pass attributes to the loop function
|
|
loop(model=input_model, serial=input_serialnumber)
|
|
|
|
else:
|
|
parser.print_help()
|
|
parser.exit(status=1, message='\nError: Not enough arguments provided.\n')
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|