Posted on 10-05-2023 11:05 AM
I've seen several posts about this, and have been struggling with it myself, but I finally found a way to create a sort of maintenance window for Patch Management policies. For our faculty and staff computers, we distribute patches through Self Service patch policies, which is fine. It leaves it to the user to patch when convenient. But for our lab and classroom computers, we don't want to rely on our students to push patches, so we use the automatic installation patch policies. They work fine, but unless the admin updates them during regularly scheduled down-time, the students are forced to quit any applications that need updates shortly after the patch policies are revised. I wanted to find a way to be able to update my patch policies, but not have them trigger until a certain time. Ironically, regular policies have an option to set up a scheduled time using the Client-Side Limitations. Sadly, patch policies do not have this option, so I set about finding a way to emulate this functionality. Here's how I created a maintenance window for my patch policies:
Create a static group as a holding space for the computers that are in a maintenance window. I simply named mine "Patch Policy Window Active."
Next, I modified a pre-existing script tp add computers to, and remove computers from a specified static group.
#!/bin/sh
##########################################################################################
#
# Static Group Membership - Computers
# Originally: Add Computer to Static Group
#
# This script uses the Jamf Pro API to get a Bearer Authentication Token and then adds a computer to a Static Group.
# In this case it could be the computer the script is run on if you uncomment the particular line. I actually set the computer name for testing to a specific name
# Adapted from https://derflounder.wordpress.com/2022/01/05/updated-script-for-obtaining-checking-and-renewing-bearer-tokens-for-the-classic-and-jamf-pro-apis/
# (not all functions in Rich's example are used as this is a very short access to the API)
# Also help from Steve Dagley in https://community.jamf.com/t5/jamf-pro/bearer-token-api-and-adding-computer-to-static-group/td-p/261269
# and https://community.jamf.com/t5/jamf-pro/script-to-add-computer-to-static-group/m-p/198738
# 2022-03-17 David London
#
# Modified for UNT by KClose
# Added parameter calls for Jamf Options.
# Updated static information to match UNT requirements.
#
# Updated 2023-09-27: Added a remove option as well and updated Parameters.
#
##########################################################################################
### VARIABLES ###
# Get the Static Group Name *REQUIRED*
[ -z "$4" ] && { echo "Static Group Name is required. Exiting."; exit 1; } || { GroupName=$4; }
# Get the Static Group ID *REQUIRED*
[ -z "$5" ] && { echo "Static Group ID is required. Exiting."; exit 1; } || { GroupID=$5; }
# Get Add/Remove. *REQUIRED*
if [[ $6 == [Aa] ]]; then
changeCommand="Add"
elif [[ $6 == [Rr] ]]; then
changeCommand="Remove"
else
echo "Change Command (A/R) is required. Exiting."
exit 1
fi
# Get the API Username. *REQUIRED*
[ -z "$7" ] && { echo "API Username is required. Exiting."; exit 1; } || { jamfpro_user=$7; }
# Get the API Password. *REQUIRED*
[ -z "$8" ] && { echo "API Password is required. Exiting."; exit 1; } || { jamfpro_password=$8; }
# Get the Jamf URL. *REQUIRED*
[ -z "$9" ] && { echo "Jamf URL is required. Exiting."; exit 1; } || { jamfpro_url=$9; }
# Remove the trailing slash from the Jamf Pro URL if needed.
jamfpro_url=${jamfpro_url%%/}
# Find/Set the computer name.
ComputerName=$(/usr/sbin/scutil --get ComputerName)
# Explicitly set initial value for the api_token variable to null:
api_token=""
# Explicitly set initial value for the token_expiration variable to null:
token_expiration=""
### FUNCTIONS ###
GetJamfProAPIToken() {
# This function uses Basic Authentication to get a new bearer token for API authentication.
# Use user account's username and password credentials with Basic Authorization to request a bearer token.
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
api_token=$(/usr/bin/curl -X POST --silent -u "${jamfpro_user}:${jamfpro_password}" "${jamfpro_url}/api/v1/auth/token" | python -c 'import sys, json; print json.load(sys.stdin)["token"]')
else
api_token=$(/usr/bin/curl -X POST --silent -u "${jamfpro_user}:${jamfpro_password}" "${jamfpro_url}/api/v1/auth/token" | plutil -extract token raw -)
fi
}
APITokenValidCheck() {
# Verify that API authentication is using a valid token by running an API command
# which displays the authorization details associated with the current API user.
# The API call will only return the HTTP status code.
api_authentication_check=$(/usr/bin/curl --write-out %{http_code} --silent --output /dev/null "${jamfpro_url}/api/v1/auth" --request GET --header "Authorization: Bearer ${api_token}")
}
InvalidateToken() {
# Verify that API authentication is using a valid token by running an API command
# which displays the authorization details associated with the current API user.
# The API call will only return the HTTP status code.
APITokenValidCheck
# If the api_authentication_check has a value of 200, that means that the current
# bearer token is valid and can be used to authenticate an API call.
if [[ ${api_authentication_check} == 200 ]]; then
# If the current bearer token is valid, an API call is sent to invalidate the token.
authToken=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/invalidate-token" --silent --header "Authorization: Bearer ${api_token}" -X POST)
# Explicitly set value for the api_token variable to null.
api_token=""
fi
}
### MAIN SCRIPT ###
GetJamfProAPIToken
apiURL="JSSResource/computergroups/id/${GroupID}"
if [[ $changeCommand == "Add" ]]; then
echo "Adding computer to $GroupName."
apiData="<computer_group><id>${GroupID}</id><name>${GroupName}</name><computer_additions><computer><name>$ComputerName</name></computer></computer_additions></computer_group>"
elif [[ "$changeCommand" == "Remove" ]]; then
echo "Removing computer from $GroupName."
apiData="<computer_group><id>${GroupID}</id><name>${GroupName}</name><computer_deletions><computer><name>$ComputerName</name></computer></computer_deletions></computer_group>"
else
echo "Something went wrong - we shouldn't see this error."
exit 1
fi
curl -s \
--header "Authorization: Bearer ${api_token}" --header "Content-Type: text/xml" \
--url "${jamfpro_url}/${apiURL}" \
--data "${apiData}" \
--request PUT > /dev/null
InvalidateToken
exit 0
The next step is to create a pair of standard policies that will add our computers to the static group, and then remove them from the group at a later time.
Lastly we can just set the scope of our patch policies to our Patch Window static group. This way the only computers that are eligible for the patch policy are those that are in the static group that is managed by our patch window policies above.
Expanding on from here, you could create several of the add/remove policies to schedule multiple windows for various groups of computers, or have static groups for specific applications. Once you have the pieces in place, the options for customization open up and give you a lot more control over your patch policies.
I hope that this helps someone because it certainly has given us more control over our patch management strategy and gives us the freedom to update the patch policies without concern that our users will be unexpectedly interrupted.