mirror of
https://github.com/MLBZ521/MacAdmin.git
synced 2026-03-02 16:42:08 +00:00
Converted the Bash version into Python...because...why not? (Mainly a reason to learn more Python.) This is a quick "Patch Management Framework" that I threw together quickly one day for a need (cough zoom cough). Not a big fan of the built in patch "features" in Jamf Pro -- they leave a bit to be desired. This, more or less, combines the functionality of the two approaches (Standard Policies and Patch Policies) and makes them appear more "user friendly."
317 lines
15 KiB
Python
317 lines
15 KiB
Python
#!/usr/bin/python
|
|
|
|
###################################################################################################
|
|
# Script Name: jamf_Patcher.py
|
|
# By: Zack Thompson / Created: 7/10/2019
|
|
# Version: 1.0.0 / Updated: 7/10/2019 / By: ZT
|
|
#
|
|
# Description: This script handles patching of applications with user notifications.
|
|
#
|
|
###################################################################################################
|
|
|
|
import os
|
|
import platform
|
|
try:
|
|
from plistlib import dump as custom_plist_Writer # For Python 3
|
|
from plistlib import load as custom_plist_Reader # For Python 3
|
|
except ImportError:
|
|
from plistlib import writePlist as custom_plist_Writer # For Python 2
|
|
from plistlib import readPlist as custom_plist_Reader # For Python 2
|
|
try:
|
|
import requests # Use requests if available
|
|
except ImportError:
|
|
try:
|
|
from urllib import request as urllib # For Python 3
|
|
except ImportError:
|
|
import urllib # For Python 2
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
|
|
def runUtility(command, errorAction='continue'):
|
|
"""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
|
|
"""
|
|
if errorAction == 'continue':
|
|
try:
|
|
process = subprocess.check_output(command, shell=True)
|
|
except:
|
|
process = "continue"
|
|
else:
|
|
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
|
|
|
|
def plistReader(plistFile):
|
|
"""A helper function to get the contents of a Property List.
|
|
Args:
|
|
plistFile: A .plist file to read in.
|
|
Returns:
|
|
stdout: Returns the contents of the plist file.
|
|
"""
|
|
|
|
if os.path.exists(plistFile):
|
|
# print('Reading {}...'.format(plistFile))
|
|
|
|
try:
|
|
plist_Contents = custom_plist_Reader(plistFile)
|
|
except Exception:
|
|
file_cmd = '/usr/bin/file --mime-encoding {}'.format(plistFile)
|
|
file_response = runUtility(file_cmd)
|
|
file_type = file_response.split(': ')[1].strip()
|
|
# print('File Type: {}'.format(file_type))
|
|
|
|
if file_type == 'binary':
|
|
# print('Converting plist...')
|
|
plutil_cmd = '/usr/bin/plutil -convert xml1 {}'.format(plistFile)
|
|
plutil_response = runUtility(plutil_cmd)
|
|
|
|
plist_Contents = custom_plist_Reader(plistFile)
|
|
else:
|
|
print('Something\'s terribly wrong here...')
|
|
|
|
return plist_Contents
|
|
|
|
def promptToPatch(**parameters):
|
|
|
|
# Prompt user to quit app.
|
|
prompt = '"{}" -windowType "{}" -title "{}" -icon "/private/tmp/{}Icon.png" -heading "{}" -description "{}" -button1 OK -timeout 3600 -countdown -countdownPrompt "If you wish to delay this patch, please make a selection in " -alignCountdown center -lockHUD -showDelayOptions ", 600, 3600, 86400"'.format(parameters.get('jamfHelper'), parameters.get('windowType'), parameters.get('title'), parameters.get('applicationName'), parameters.get('heading'), parameters.get('description'))
|
|
selection = runUtility(prompt)
|
|
print('SELECTION: {}'.format(selection))
|
|
|
|
if selection == "1":
|
|
print('User selected to patch now.')
|
|
killAndInstall(**parameters)
|
|
|
|
elif selection[:-1] == "600":
|
|
print('DELAY: 600 seconds')
|
|
delayDaemon(delayTime=600, **parameters)
|
|
|
|
elif selection[:-1] == "3600":
|
|
print('DELAY: 3600 seconds')
|
|
delayDaemon(delayTime=3600, **parameters)
|
|
|
|
elif selection[:-1] == "86400":
|
|
print('DELAY: 86400 seconds')
|
|
delayDaemon(delayTime=86400, **parameters)
|
|
|
|
elif selection == "243":
|
|
print('TIMED OUT: user did not make a selection')
|
|
killAndInstall(**parameters)
|
|
|
|
else:
|
|
print('Unknown action was taken at prompt.')
|
|
killAndInstall(**parameters)
|
|
|
|
def killAndInstall(**parameters):
|
|
|
|
try:
|
|
print('Attempting to close app if it\'s running...')
|
|
# Get PID of the application
|
|
print('Process ID: {}'.format(parameters.get('status').split(' ')[0]))
|
|
pid = int(parameters.get('status').split(' ')[0])
|
|
|
|
# Kill PID
|
|
os.kill(pid, signal.SIGTERM) #or signal.SIGKILL
|
|
except:
|
|
print('Unable to terminate app, assuming it was manually closed...')
|
|
|
|
print('Performing install...')
|
|
|
|
# Run Policy
|
|
runUtility(parameters.get('installPolicy'))
|
|
# print('Test run, don\'t run policy!')
|
|
|
|
prompt = '"{}" -windowType "{}" -title "{}" -icon "/private/tmp/{}Icon.png" -heading "{}" -description "{}" -button1 OK -timeout 60 -alignCountdown center -lockHUD'.format(parameters.get('jamfHelper'), parameters.get('windowType'), parameters.get('title'), parameters.get('applicationName'), parameters.get('heading'), parameters.get('descriptionComplete'))
|
|
runUtility(prompt)
|
|
|
|
def delayDaemon(**parameters):
|
|
|
|
# Configure for delay.
|
|
if os.path.exists(parameters.get('patchPlist')):
|
|
patchPlist_contents = plistReader(parameters.get('patchPlist'))
|
|
patchPlist_contents.update( { parameters.get('applicationName') : "Delayed" } )
|
|
custom_plist_Writer(patchPlist_contents, parameters.get('patchPlist'))
|
|
else:
|
|
patchPlist_contents = { parameters.get('applicationName') : "Delayed" }
|
|
custom_plist_Writer(patchPlist_contents, parameters.get('patchPlist'))
|
|
|
|
print('Creating the Patcher launchDaemon...')
|
|
|
|
launchDaemon_plist = {
|
|
"Label" : "com.github.mlbz521.jamf.patcher",
|
|
'ProgramArguments' : ["/usr/local/jamf/bin/jamf", "policy", "-id", "{}".format(parameters.get('patchID'))],
|
|
'StartInterval' : parameters.get('delayTime'),
|
|
'AbandonProcessGroup' : True
|
|
}
|
|
|
|
custom_plist_Writer(launchDaemon_plist, parameters.get('launchDaemonLocation'))
|
|
|
|
if os.path.exists(parameters.get('launchDaemonLocation')):
|
|
|
|
# Check if the LaucnhDaemon is running, if so restart it in case a change was made to the plist file.
|
|
# Determine proper launchctl syntax based on OS Version
|
|
if parameters.get('osMinorVersion') >= 11:
|
|
launchctl_print = '/bin/launchctl print system/{} > /dev/null 2>&1; echo $?'.format(parameters.get('launchDaemonLabel'))
|
|
exitCode = runUtility(launchctl_print)
|
|
# print('exitCode: {}'.format(exitCode))
|
|
|
|
if int(exitCode) == 0:
|
|
print('Patcher launchDaemon is currently started; stopping now...')
|
|
launchctl_bootout = '/bin/launchctl bootout system/{}'.format(parameters.get('launchDaemonLabel'))
|
|
runUtility(launchctl_bootout)
|
|
|
|
print('Loading Patcher launchDaemon...')
|
|
launchctl_bootstrap = '/bin/launchctl bootstrap system {}'.format(parameters.get('launchDaemonLocation'))
|
|
runUtility(launchctl_bootstrap)
|
|
|
|
launchctl_enable = '/bin/launchctl enable system/{}'.format(parameters.get('launchDaemonLabel'))
|
|
runUtility(launchctl_enable)
|
|
|
|
elif parameters.get('osMinorVersion') <= 10:
|
|
launchctl_list = '/bin/launchctl list {} > /dev/null 2>&1; echo $?'.format(parameters.get('launchDaemonLabel'))
|
|
exitCode = runUtility(launchctl_list)
|
|
|
|
if int(exitCode) == 0:
|
|
print('Patcher launchDaemon is currently started; stopping now...')
|
|
launchctl_unload = '/bin/launchctl unload {}'.format(parameters.get('launchDaemonLocation'))
|
|
runUtility(launchctl_unload)
|
|
|
|
print('Loading Patcher launchDaemon...')
|
|
launchctl_enable = '/bin/launchctl load {}'.format(parameters.get('launchDaemonLocation'))
|
|
runUtility(launchctl_enable)
|
|
|
|
def cleanUp(**parameters):
|
|
print('Performing cleanup...')
|
|
|
|
# Clean up patchPlist.
|
|
if os.path.exists(parameters.get('patchPlist')):
|
|
patchPlist_contents = plistReader(parameters.get('patchPlist'))
|
|
|
|
if patchPlist_contents.get(parameters.get('applicationName')):
|
|
patchPlist_contents.pop(parameters.get('applicationName'), None)
|
|
print('Removing previously delayed app: {}'.format(parameters.get('applicationName')))
|
|
custom_plist_Writer(patchPlist_contents, parameters.get('patchPlist'))
|
|
else:
|
|
print('App not listed in patchPlist: {}'.format(parameters.get('applicationName')))
|
|
|
|
# Check if the LaunchDaemon is running.
|
|
# Determine proper launchctl syntax based on OS Version.
|
|
if parameters.get('osMinorVersion') >= 11:
|
|
launchctl_print = '/bin/launchctl print system/{} > /dev/null 2>&1; echo $?'.format(parameters.get('launchDaemonLabel'))
|
|
exitCode = runUtility(launchctl_print)
|
|
|
|
if int(exitCode) == 0:
|
|
print('Stopping the Patcher launchDaemon...')
|
|
launchctl_bootout = '/bin/launchctl bootout system/{}'.format(parameters.get('launchDaemonLabel'))
|
|
runUtility(launchctl_bootout)
|
|
|
|
elif parameters.get('osMinorVersion') <= 10:
|
|
launchctl_list = '/bin/launchctl list {} > /dev/null 2>&1; echo $?'.format(parameters.get('launchDaemonLabel'))
|
|
exitCode = runUtility(launchctl_list)
|
|
|
|
if int(exitCode) == 0:
|
|
print('Stopping the Patcher launchDaemon...')
|
|
launchctl_unload = '/bin/launchctl unload {}'.format(parameters.get('launchDaemonLabel'))
|
|
runUtility(launchctl_unload)
|
|
|
|
if os.path.exists(parameters.get('launchDaemonLocation')):
|
|
os.remove(parameters.get('launchDaemonLocation'))
|
|
|
|
def main():
|
|
print('***** jamf_Patcher process: START *****')
|
|
|
|
##################################################
|
|
# Define Script Parameters
|
|
print('All args: {}'.format(sys.argv))
|
|
departmentName = sys.argv[4] # "My Organization Technology Office"
|
|
applicationName = sys.argv[5] # "zoom"
|
|
iconID = sys.argv[6] # "https://jps.server.com:8443/icon?id=49167"
|
|
patchID = sys.argv[7]
|
|
policyID = sys.argv[8]
|
|
|
|
##################################################
|
|
# Define Variables
|
|
jamfPS = plistReader('/Library/Preferences/com.jamfsoftware.jamf.plist')['jss_url']
|
|
patchPlist = '/Library/Preferences/com.github.mlbz521.jamf.patcher.plist'
|
|
jamfHelper = '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper'
|
|
launchDaemonLabel = 'com.github.mlbz521.jamf.patcher.{}'.format(applicationName)
|
|
launchDaemonLocation = '/Library/LaunchDaemons/{}.plist'.format(launchDaemonLabel)
|
|
osMinorVersion = platform.mac_ver()[0].split('.')[1]
|
|
installPolicy = '/usr/local/jamf/bin/jamf policy -id {}'.format(policyID)
|
|
|
|
##################################################
|
|
# Define jamfHelper window values
|
|
title="Security Patch Notification"
|
|
windowType="hud"
|
|
description = '{name} will be updated to patch a security vulnerability. Please quit {name} to apply this update.\n\nIf you have questions, please contact your deskside support group.'.format(name=applicationName)
|
|
descriptionForce = '{name} will be updated to patch a security vulnerability. Please quit {name} within the allotted time to apply this update.\n\nIf you have questions, please contact your deskside support group.'.format(name=applicationName)
|
|
descriptionComplete = '{} has been patched!\n\n.'.format(applicationName)
|
|
icon = '/private/tmp/{}Icon.png'.format(applicationName)
|
|
|
|
if departmentName:
|
|
heading = 'My Organization - {}'.format(departmentName)
|
|
else:
|
|
heading = 'My Organization'
|
|
|
|
##################################################
|
|
# Bits staged...
|
|
|
|
psCheck = '/bin/ps -ax -o pid,command | /usr/bin/grep -E "/Applications/{}" | /usr/bin/grep -v "grep" 2> /dev/null'.format(applicationName)
|
|
status = runUtility(psCheck)
|
|
print('APP STATUS: {}'.format(status))
|
|
|
|
if status == "continue":
|
|
print('{} is not running, installing now...'.format(applicationName))
|
|
runUtility(installPolicy)
|
|
# print('Test run, don\'t run policy!')
|
|
|
|
cleanUp(applicationName=applicationName, patchPlist=patchPlist, launchDaemonLabel=launchDaemonLabel, launchDaemonLocation=launchDaemonLocation, osMinorVersion=osMinorVersion)
|
|
|
|
else:
|
|
print('{} is running...'.format(applicationName))
|
|
|
|
# Download the icon from the JPS
|
|
iconURL = '{}icon?id={}'.format(jamfPS, iconID)
|
|
try:
|
|
iconImage = requests.get(iconURL)
|
|
open('/private/tmp/{}Icon.png'.format(applicationName), 'wb').write(iconImage.content)
|
|
except:
|
|
sys.exc_clear()
|
|
urllib.urlretrieve(iconURL, filename='/private/tmp/{}Icon.png'.format(applicationName))
|
|
|
|
if os.path.exists(patchPlist):
|
|
patchPlist_contents = plistReader(patchPlist)
|
|
delayCheck = patchPlist_contents.get(applicationName)
|
|
|
|
if delayCheck:
|
|
print('STATUS: Patch has already been delayed; forcing upgrade.')
|
|
|
|
# Prompt user with one last warning.
|
|
prompt = '"{}" -windowType "{}" -title "{}" -icon "/private/tmp/{}Icon.png" -heading "{}" -description "{}" -button1 OK -timeout 600 -countdown -countdownPrompt \'{} will be force closed in \' -alignCountdown center -lockHUD > /dev/null 2>&1'.format(jamfHelper, windowType, title, applicationName, heading, descriptionForce, applicationName)
|
|
runUtility(prompt)
|
|
|
|
killAndInstall(status=status, installPolicy=installPolicy, applicationName=applicationName, jamfHelper=jamfHelper, windowType=windowType, title=title, heading=heading, descriptionComplete=descriptionComplete)
|
|
|
|
cleanUp(applicationName=applicationName, patchPlist=patchPlist, launchDaemonLabel=launchDaemonLabel, launchDaemonLocation=launchDaemonLocation, osMinorVersion=osMinorVersion, patchPlist_contents=patchPlist_contents)
|
|
|
|
else:
|
|
print('STATUS: Patch has not been delayed; prompting user.')
|
|
promptToPatch(applicationName=applicationName, patchID=patchID, patchPlist=patchPlist, jamfHelper=jamfHelper, windowType=windowType, title=title, heading=heading, description=description, descriptionComplete=descriptionComplete, launchDaemonLabel=launchDaemonLabel, launchDaemonLocation=launchDaemonLocation, osMinorVersion=osMinorVersion, status=status, installPolicy=installPolicy)
|
|
else:
|
|
print('STATUS: Patch has not been delayed; prompting user.')
|
|
promptToPatch(applicationName=applicationName, patchID=patchID, patchPlist=patchPlist, jamfHelper=jamfHelper, windowType=windowType, title=title, heading=heading, description=description, descriptionComplete=descriptionComplete, launchDaemonLabel=launchDaemonLabel, launchDaemonLocation=launchDaemonLocation, osMinorVersion=osMinorVersion, status=status, installPolicy=installPolicy)
|
|
|
|
print('***** jamf_Patcher process: SUCCESS *****')
|
|
|
|
if __name__ == "__main__":
|
|
main()
|