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."
This commit is contained in:
Zack T
2019-08-23 23:58:29 -07:00
parent 5671b90fe4
commit 38f9338249

View File

@@ -0,0 +1,316 @@
#!/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()