Files
MacAdmin/Apple_RepairPrograms.py
Zack T 3ea5985a77 v1.1.0 = Added new repair programs
+ Add new repair programs
- Removed discontinued programs
2021-02-18 23:51:00 -07:00

284 lines
12 KiB
Python

#!/usr/bin/python
"""
###################################################################################################
# Script Name: Apple_RepairPrograms.py
# By: Zack Thompson / Created: 8/24/2019
# Version: 1.1.0 / Updated: 2/12/2021 / 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 11":
# iPhone 11 Display Module Replacement Program for Touch Issues
# https://support.apple.com/iphone-11-display-module-replacement-program
program_number.append("122020")
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" or model == "iPhone 6 Plus":
# iPhone 6s and iPhone 6s Plus Service Program for No Power Issues
# https://support.apple.com/iphone-6s-6s-plus-no-power-issues-program
program_number.append("102019")
# Discontinued programs
# 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()