Last active
March 25, 2024 23:25
-
-
Save grahampugh/836547859c18fefe1dba6ba8c093accc to your computer and use it in GitHub Desktop.
JCDS2 package upload script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
: <<DESCRIPTION | |
JCDS2 package upload script | |
by Graham Pugh (@grahamrpugh) | |
1. Get a bearer token | |
2. Check for an existing package from the Classic API | |
3. Check for an existing package from the JCDS endpoint | |
4. Check that the checksum matches our local pkg | |
5. Delete the existing package from the JCDS if the checksum doesn't match | |
6. Upload package to JCDS if it's new or the existing has been deleted (but not if it has the same hash) | |
7. Upload the package metadata | |
Note: requires the aws-cli tools to be installed | |
DESCRIPTION | |
## --------------------------------------------------------------- | |
## VARIABLES | |
# declarations | |
token="" | |
expiration_epoch="0" | |
# files | |
output_location="/tmp/api_tests" | |
mkdir -p "$output_location" | |
cookie_jar="$output_location/cookie_jar.txt" | |
headers_file_session="$output_location/headers_session.txt" | |
headers_file_token="$output_location/headers_token.txt" | |
headers_file_record="$output_location/headers_record.txt" | |
headers_file_list="$output_location/headers_list.txt" | |
headers_file_delete="$output_location/headers_delete.txt" | |
output_file_session="$output_location/output_session.txt" | |
output_file_token="$output_location/output_token.txt" | |
output_file_record="$output_location/output_record.txt" | |
output_file_list="$output_location/output_list.txt" | |
output_file_delete="$output_location/output_delete.txt" | |
## --------------------------------------------------------------- | |
## FUNCTIONS | |
usage() { | |
echo "Usage: api_test_pkg_jcds2.sh --jss someserver --user username --pass password --pkg /path/to/pkg.pkg" | |
echo "(don't include https:// or .jamfcloud.com)" | |
echo | |
echo "Use --replace to replace an existing package" | |
} | |
getBearerToken() { | |
# generate a b64 hash of the credentials | |
credentials=$(printf "%s" "$user:$pass" | iconv -t ISO-8859-1 | base64 -i -) | |
# request the token | |
http_response=$( | |
curl --request POST \ | |
--silent \ | |
--header "authorization: Basic $credentials" \ | |
--url "$url/api/v1/auth/token" \ | |
--write-out "%{http_code}" \ | |
--header 'Accept: application/json' \ | |
--cookie-jar "$cookie_jar" \ | |
--dump-header "$headers_file_token" \ | |
--output "$output_file_token" | |
) | |
echo "HTTP response: $http_response" | |
} | |
checkTokenExpiration() { | |
if [[ -f "$output_file_token" ]]; then | |
echo "Token file found" | |
token=$(plutil -extract token raw "$output_file_token") | |
expires=$(plutil -extract expires raw "$output_file_token" | awk -F . '{print $1}') | |
expiration_epoch=$(date -j -f "%Y-%m-%dT%T" "$expires" +"%s") | |
else | |
echo "No token file found" | |
fi | |
utc_epoch_now=$(date -j -f "%Y-%m-%dT%T" "$(date -u +"%Y-%m-%dT%T")" +"%s") | |
if [[ expiration_epoch -gt utc_epoch_now ]]; then | |
echo "Token valid until the following epoch time: " "$expiration_epoch" | |
else | |
echo "No valid token available, getting new token" | |
getBearerToken | |
token=$(plutil -extract token raw "$output_file_token") | |
fi | |
} | |
invalidateToken() { | |
response=$(curl -w "%{http_code}" -H "Authorization: Bearer $token" "$url/api/v1/auth/invalidate-token" -X POST -s -o /dev/null) | |
if [[ $response == 204 ]]; then | |
echo "Token successfully invalidated" | |
token="" | |
expiration_epoch="0" | |
elif [[ $response == 401 ]]; then | |
echo "Token already invalid" | |
else | |
echo "An unknown error occurred invalidating the token" | |
fi | |
} | |
checkExistingPackage() { | |
# perform a get request on an existing package to see if it exists | |
echo "Seeing if $pkg exists on the server" | |
http_response=$( | |
curl --request GET \ | |
--silent \ | |
--header "authorization: Bearer $token" \ | |
--header 'Accept: application/json' \ | |
"$url/JSSResource/packages/name/$pkg" \ | |
--write-out "%{http_code}" \ | |
--location \ | |
--cookie-jar "$cookie_jar" \ | |
--dump-header "$headers_file_record" \ | |
--output "$output_file_record" | |
) | |
echo "HTTP response: $http_response" | |
if [[ $http_response -lt 350 && $http_response -ge 100 ]]; then | |
pkg_id=$(plutil -extract package.id raw -expect integer "$output_file_record") | |
# check that we got an integer | |
if [ "$pkg_id" -eq "$pkg_id" ]; then | |
echo "Existing package found: ID $pkg_id" | |
if [[ $replace ]]; then | |
echo "Replacing existing package" | |
else | |
echo "Not replacing existing package" | |
exit | |
fi | |
fi | |
else | |
# TODO - give better descriptions based on HTTP response | |
echo "No existing package found: uploading as a new package" | |
pkg_id=0 | |
fi | |
} | |
deleteExistingPkg() { | |
# delete the existing package | |
echo "Deleting $pkg from JCDS" | |
http_response=$( | |
curl --request DELETE \ | |
--silent \ | |
--header "authorization: Bearer $token" \ | |
--header 'Accept: application/json' \ | |
"$url/api/v1/jcds/files/$pkg" \ | |
--write-out "%{http_code}" \ | |
--location \ | |
--cookie-jar "$cookie_jar" \ | |
--dump-header "$headers_file_delete" \ | |
--output "$output_file_delete" | |
) | |
echo "HTTP response: $http_response" | |
} | |
checkJCDS() { | |
# list all the packages | |
echo "Getting a list of packages from JCDS" | |
http_response=$( | |
curl --request GET \ | |
--silent \ | |
--header "authorization: Bearer $token" \ | |
--header 'Accept: application/json' \ | |
"$url/api/v1/jcds/files" \ | |
--write-out "%{http_code}" \ | |
--location \ | |
--cookie-jar "$cookie_jar" \ | |
--dump-header "$headers_file_list" \ | |
--output "$output_file_list" | |
) | |
echo "HTTP response: $http_response" | |
if [[ $http_response -eq 200 ]]; then | |
# convert the list to a plist so we can actually work with it in bash | |
plutil -convert xml1 "$output_file_list" | |
# count the number of items in the list | |
pkg_count=$(grep -c fileName "$output_file_list") | |
# loop through each item in the JSON response | |
jcds_pkg="" | |
jcds_pkg_md5="" # assign empty value to avoid errors | |
for ((i=0; i<=pkg_count; i++)); do | |
jcds_pkg=$(/usr/libexec/PlistBuddy -c "Print :$i:fileName" "$output_file_list" 2>/dev/null) | |
if [[ "$jcds_pkg" == "$pkg" ]]; then | |
jcds_pkg_md5=$(/usr/libexec/PlistBuddy -c "Print :$i:md5" "$output_file_list") | |
break | |
fi | |
done | |
# also find out the sha3 of the local package | |
pkg_md5=$(md5 -q "$pkg_path") | |
# now compare the two | |
if [[ "$jcds_pkg_md5" ]]; then | |
echo "Existing package found: URL $jcds_pkg" | |
if [[ $replace ]]; then | |
# Check if the MD5 hash matches (Mac's LibreSSL can't do SHA3-512) | |
# If not, we want to replace it, which has to be done | |
# by deleting and uploading new | |
if [[ "$jcds_pkg_md5" == "$pkg_md5" ]]; then | |
echo "MD5 matches so not replacing existing package on JCDS" | |
replace_jcds_pkg=0 | |
else | |
echo "MD5 hash doesn't match. Replacing existing package on JCDS" | |
replace_jcds_pkg=1 | |
fi | |
else | |
echo "Not replacing existing package on JCDS" | |
replace_jcds_pkg=0 | |
fi | |
fi | |
else | |
echo "No existing package found: uploading as a new package" | |
fi | |
} | |
getPkgUploadToken() { | |
# get an access token to a package to the JCDS | |
echo "Getting a package upload token from JCDS" | |
http_response=$( | |
curl --request POST \ | |
--silent \ | |
--header "authorization: Bearer $token" \ | |
--header 'Accept: application/json' \ | |
--header 'Content-Type: application/json' \ | |
"$url/api/v1/jcds/files" \ | |
--write-out "%{http_code}" \ | |
--location \ | |
--cookie-jar "$cookie_jar" \ | |
--dump-header "$headers_file_session" \ | |
--output "$output_file_session" \ | |
) | |
echo | |
echo "HTTP response: $http_response" | |
} | |
postPkg() { | |
# upload the package to an S3 bucket - requires aws-cli tools | |
# set the required configurations (delete these afterwards) | |
default_access_key=$(plutil -extract accessKeyID raw "$output_file_session") | |
default_secret_key=$(plutil -extract secretAccessKey raw "$output_file_session") | |
aws_session_token=$(plutil -extract sessionToken raw "$output_file_session") | |
region=$(plutil -extract region raw "$output_file_session") | |
s3_bucket=$(plutil -extract bucketName raw "$output_file_session") | |
s3_path=$(plutil -extract path raw "$output_file_session") | |
# add the configuration to the aws-cli config file | |
aws configure set aws_access_key_id "$default_access_key" | |
aws configure set aws_secret_access_key "$default_secret_key" | |
aws configure set aws_session_token "$aws_session_token" | |
aws configure set default.region "$region" | |
# post the package | |
aws s3 cp "$pkg_path" "s3://$s3_bucket/$s3_path" --region "$region" | |
# delete credentials from the aws-cli config file | |
aws configure set aws_access_key_id "" | |
aws configure set aws_secret_access_key "" | |
aws configure set aws_session_token "" | |
aws configure set default.region "" | |
} | |
postMetadata() { | |
# post the metadata to match the package | |
pkg_data="<package> | |
<name>$pkg</name> | |
<filename>$pkg</filename> | |
</package>" | |
echo "Posting the package metadata to the server" | |
if [[ $pkg_id -gt 0 ]]; then | |
req="PUT" | |
else | |
req="POST" | |
fi | |
http_response=$( | |
curl --request "$req" \ | |
--silent \ | |
--header "authorization: Bearer $token" \ | |
--header 'Content-Type: application/xml' \ | |
--data "$pkg_data" \ | |
"$url/JSSResource/packages/id/$pkg_id" \ | |
--write-out "%{http_code}" \ | |
--location \ | |
--cookie-jar "$cookie_jar" \ | |
--dump-header "$headers_file_record" \ | |
--output "$output_file_record" | |
) | |
echo | |
echo "HTTP response: $http_response" | |
if [[ "$http_response" == "10"* || "$http_response" == "20"* ]]; then | |
echo "Success response ($http_response)" | |
else | |
echo "Fail response ($http_response)" | |
fi | |
} | |
## --------------------------------------------------------------- | |
## START | |
while test $# -gt 0 ; do | |
case "$1" in | |
-s|--jss) | |
shift | |
jss="$1" | |
;; | |
-u|--user) | |
shift | |
user="$1" | |
;; | |
-p|--pass) | |
shift | |
pass="$1" | |
;; | |
-f|--pkg) | |
shift | |
pkg_path="$1" | |
;; | |
--replace) | |
shift | |
replace=1 | |
;; | |
*) | |
usage | |
exit | |
;; | |
esac | |
shift | |
done | |
if [[ ! $jss || ! $user || ! $pass || ! $pkg_path ]]; then | |
usage | |
exit | |
fi | |
# set URL | |
url="https://$jss.jamfcloud.com" | |
# set pkg name | |
pkg=$(basename "$pkg_path") | |
# grab a token | |
checkTokenExpiration | |
## --------------------------------------------------------------- | |
## MAIN | |
# check there's an existing package in Jamf Pro | |
checkExistingPackage | |
# check if that same package exists in JCDS | |
checkJCDS | |
# if --replace and the package metadata doesn't match, delete the existing package from the JCDS | |
if [[ $replace_jcds_pkg -eq 1 ]]; then | |
deleteExistingPkg | |
fi | |
# upload the package if it's new or we just deleted the existing one | |
if [[ ! "$jcds_pkg_md5" || $replace_jcds_pkg -eq 1 ]]; then | |
getPkgUploadToken | |
postPkg | |
fi | |
# post the package metadata to Jamf Pro | |
postMetadata | |
## --------------------------------------------------------------- | |
## END | |
# finish up by expiring the token | |
echo | |
invalidateToken | |
rm "$output_file_token" |
Nice. When looking for the MD5 hash shouldn't the loop start at 0 rather then 1.
line 185, change: for ((i=1; i<=pkg_count; i++)); do
to: for ((i=0; i<pkg_count; i++)); do
And so that if [[ "$jcds_pkg_md5" ]]; then
evaluates as desired
line 184, change jcds_pkg_md5=0 # assign empty value to avoid errors
to jcds_pkg_md5="" # assign empty value to avoid errors
@BIG-RAT thanks, I've made those changes.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a bare-minimum script for uploading a package to a Jamf Pro instance with a JCDS2 distribution point.
The aws-cli tool is required to be installed.