Files
MacAdmin/Jamf Pro/Scripts/jamf_Patcher.py
Zack T 38f9338249 v1.0.0 = Initial Version
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."
2019-08-23 23:58:29 -07:00

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()