JSS API PUT data

luispalumbo
Contributor

Hi guys,

I'm trying to improve our system adding the warranty expiry date to all computers in JSS without using Apple GSX.

I had searched and tried a lot of things already, mainly with shell script, and I have finally understood how to use GET easily with curl. I can now get the computer information I want, however I wanted to insert the date using PUT but I couldn't understand how. I have followed this discussion https://jamfnation.jamfsoftware.com/discussion.html?id=5968 using xpath, which once again, helped a lot.

Does anybody here use the API to PUT data into the system?

Thanks in advance.
Luis

1 ACCEPTED SOLUTION

stevewood
Honored Contributor II
Honored Contributor II

I have actually been working on something similar, only I wanted to update the lease expiration data for my computers. Using the API made the most sense, so I did a search of JAMF Nation and came up with this discussion:

API and POST command

Using the script that is in there, I was able to cobble together a script that reads from a CSV file and updates the lease expiration data for each computer in the list. Because the API uses the computer ID for PUT requests, I had to use the serial number to find the ID of the computer. That's why there are two calls to the API: a GET and a POST.

I'm sure there are cleaner ways to do this, this is my first foray into the API, and I'm sure someone on list will let me know how to do it better (please do). Hopefully it will help you a little. Here is the script I have so far:

#!/bin/sh

# Name: leaseupdate.sh
# Date: 9 March 2015
# Author: Steve Wood (swood@integer.com) 
# Purpose: used to read in lease data from a CSV file and update the record in the JSS
# The CSV file needs to be saved as a UNIX file with LF, not CR
# Version: 1.0
#
# A good portion of this script is re-purposed from the script posted in the following JAMF Nation article:
#
#  https://jamfnation.jamfsoftware.com/discussion.html?id=13118#respond
#

jssAPIUsername="<apiuser>"
jssAPIPassword="<apipassword>"
jssAddress="https://your.jss.com:8443"
file="<path-to-csv-file>"

#Verify we can read the file
data=`cat $file`
if [[ "$data" == "" ]]; then
    echo "Unable to read the file path specified"
    echo "Ensure there are no spaces and that the path is correct"
    exit 1
fi

#Find how many computers to import
computerqty=`awk -F, 'END {printf "%s
", NR}' $file`
echo "Computerqty= " $computerqty
#Set a counter for the loop
counter="0"

duplicates=[]

id=$((id+1))

#Loop through the CSV and submit data to the API
while [ $counter -lt $computerqty ]
do
    counter=$[$counter+1]
    line=`echo "$data" | head -n $counter | tail -n 1`
    serialNumber=`echo "$line" | awk -F , '{print $1}'`
    leaseExpires=`echo "$line" | awk -F , '{print $2}'`

    echo "Attempting to update lease data for $serialNumber"

    # use serialNumber to locate the ID of the comptuer
    ### Add some logic to test for an empty set coming back from serial number check.
    myOutput=`curl -su ${jssAPIUsername}:${jssAPIPassword} -X GET ${jssAddress}/JSSResource/computers/serialnumber/$serialNumber`
    myResult=`echo $myOutput | xpath /computer/general/id[1] | awk -F'>|<' '/id/{print $3}'`
    myID=`echo $myResult | tail -1`

    echo $serialNumber " " $myID " " $leaseExpires
    apiData="<computer><purchasing><lease_expires>$leaseExpires</lease_expires></purchasing></computer>"
    output=`curl -sS -k -i -u ${jssAPIUsername}:${jssAPIPassword} -X PUT -H "Content-Type: text/xml" -d "<?xml version="1.0" encoding="ISO-8859-1"?>$apiData" ${jssAddress}/JSSResource/computers/id/$myID`

    echo $output
    #Error Checking
    error=""
    error=`echo $output | grep "Conflict"`
    if [[ $error != "" ]]; then
        duplicates+=($serialnumber)
    fi
    #Increment the ID variable for the next user
    id=$((id+1))
done

echo "The following computers could not be created:"
printf -- '%s
' "${duplicates[@]}"

exit 0

And here's a link to it on my GitHub repo: leaseUpdate Script

View solution in original post

25 REPLIES 25

Josh_S
Contributor III

Using bash, with the "xmlFile" variable being the path to an xml file with the information you'd like to update, it looks like the following command. You can omit any data from the xml file that you wouldn't like to have modified. Note: the "-s" switch makes it silent and "-k" makes it ignore ssl certificate errors. If you don't need/want this, you can also omit them.

curl -s -k -u "${apiUsername}:${apiPassword}" -H "Content-Type: application/xml" -X "PUT" -d "@${xmlFile}" "${jssServer}/JSSResource/computers/id/${computerID}"

If you'd like to see it in actual usage. I have a couple public projects that you can reference:
https://github.com/Expedia-IT-CTE/radar
https://github.com/Expedia-IT-CTE/git-ception

stevewood
Honored Contributor II
Honored Contributor II

I have actually been working on something similar, only I wanted to update the lease expiration data for my computers. Using the API made the most sense, so I did a search of JAMF Nation and came up with this discussion:

API and POST command

Using the script that is in there, I was able to cobble together a script that reads from a CSV file and updates the lease expiration data for each computer in the list. Because the API uses the computer ID for PUT requests, I had to use the serial number to find the ID of the computer. That's why there are two calls to the API: a GET and a POST.

I'm sure there are cleaner ways to do this, this is my first foray into the API, and I'm sure someone on list will let me know how to do it better (please do). Hopefully it will help you a little. Here is the script I have so far:

#!/bin/sh

# Name: leaseupdate.sh
# Date: 9 March 2015
# Author: Steve Wood (swood@integer.com) 
# Purpose: used to read in lease data from a CSV file and update the record in the JSS
# The CSV file needs to be saved as a UNIX file with LF, not CR
# Version: 1.0
#
# A good portion of this script is re-purposed from the script posted in the following JAMF Nation article:
#
#  https://jamfnation.jamfsoftware.com/discussion.html?id=13118#respond
#

jssAPIUsername="<apiuser>"
jssAPIPassword="<apipassword>"
jssAddress="https://your.jss.com:8443"
file="<path-to-csv-file>"

#Verify we can read the file
data=`cat $file`
if [[ "$data" == "" ]]; then
    echo "Unable to read the file path specified"
    echo "Ensure there are no spaces and that the path is correct"
    exit 1
fi

#Find how many computers to import
computerqty=`awk -F, 'END {printf "%s
", NR}' $file`
echo "Computerqty= " $computerqty
#Set a counter for the loop
counter="0"

duplicates=[]

id=$((id+1))

#Loop through the CSV and submit data to the API
while [ $counter -lt $computerqty ]
do
    counter=$[$counter+1]
    line=`echo "$data" | head -n $counter | tail -n 1`
    serialNumber=`echo "$line" | awk -F , '{print $1}'`
    leaseExpires=`echo "$line" | awk -F , '{print $2}'`

    echo "Attempting to update lease data for $serialNumber"

    # use serialNumber to locate the ID of the comptuer
    ### Add some logic to test for an empty set coming back from serial number check.
    myOutput=`curl -su ${jssAPIUsername}:${jssAPIPassword} -X GET ${jssAddress}/JSSResource/computers/serialnumber/$serialNumber`
    myResult=`echo $myOutput | xpath /computer/general/id[1] | awk -F'>|<' '/id/{print $3}'`
    myID=`echo $myResult | tail -1`

    echo $serialNumber " " $myID " " $leaseExpires
    apiData="<computer><purchasing><lease_expires>$leaseExpires</lease_expires></purchasing></computer>"
    output=`curl -sS -k -i -u ${jssAPIUsername}:${jssAPIPassword} -X PUT -H "Content-Type: text/xml" -d "<?xml version="1.0" encoding="ISO-8859-1"?>$apiData" ${jssAddress}/JSSResource/computers/id/$myID`

    echo $output
    #Error Checking
    error=""
    error=`echo $output | grep "Conflict"`
    if [[ $error != "" ]]; then
        duplicates+=($serialnumber)
    fi
    #Increment the ID variable for the next user
    id=$((id+1))
done

echo "The following computers could not be created:"
printf -- '%s
' "${duplicates[@]}"

exit 0

And here's a link to it on my GitHub repo: leaseUpdate Script

luispalumbo
Contributor

Thanks @stevewood ,

The PUT command worked fine.

I noticed you are using the computer id to insert data, but you can actually use the serial number itself, which worked for me.

In your output you can change the end of the line to

output=`curl -sS -k -i -u ${jssAPIUsername}:${jssAPIPassword} -X PUT -H "Content-Type: text/xml" -d "<?xml version="1.0" encoding="ISO-8859-1"?>$apiData" ${jssAddress}/JSSResource/computers/serialnumber/$serialnumber`

Thanks for the help guys.

stevewood
Honored Contributor II
Honored Contributor II

Thanks for that info @luispalumbo! I hadn't tried using the serial number since I didn't see that as an example in the API documentation. Like I said, I'm new to playing around with the API.

luispalumbo
Contributor

@stevewood if you look at the implementation notes of that PUT command API:

Implementation Notes You can PUT using the resource URLs with parameters of {name}, {udid}, {serial number}, or {macaddress}.

And that's the same for POST and DELETE commands.

I'm trying to create a script that automates it pulling serial number from JSS and then inserting it without using a CSV file like you do.

I can post it here once I've finished with that. I'm also trying to get the warranty date straight from Apple selfsolve page and insert it automatically into JSS.

Thanks once again.

luispalumbo
Contributor

@stevewood This is the script I've created to check the computer warranty date from Apple selfsove page.

#!/bin/bash
#
# Script created by Luis Fernando Palumbo de Oliveira
# Iona Presentation College - 12/03/2014
#
# Description of the script
# This script checks for Apple Warranty date for a specific computer then updates this information
# on JSS using its APIs

#############
# FUNCTIONS #
#############
CheckWarranty() {
    curl -k -s "https://selfsolve.apple.com/wcResults.do?sn=${serialNumber}&Continue=Continue&num=0" -o $outputFile
    warrantyDate=`cat $outputFile | grep "warrantyPage.warrantycheck.displayHWSupportInfo" | awk -F '<br/>' '{print $2}' | awk -F 'Date: ' '{print $2}'`
    warrantyExpiresApple=`date -jf '%B %d, %Y' "$warrantyDate" +%Y-%m-%d`
    # XML file contents that will be used by the PUT command
    apiData="<?xml version="1.0" encoding="ISO-8859-1"?><computer><purchasing><warranty_expires>${warrantyExpiresApple}</warranty_expires></purchasing></computer>"
}

PUTCommand() {
    #PUT command
    curl -sS -k -i --user ${jssAPIUser}:${jssAPIPassword} -X PUT -H "Content-Type: text/xml" -d "${apiData}" "${jssURL}/computers/serialnumber/${serialNumber}"
}


##############
# CONSTANTS #
##############
jssAPIUser="username"
jssAPIPassword="password"
jssURL="https://jss.server:8443/JSSResource"
# Saves Apple warranty query data into this file
outputFile="/tmp/warranty.xml"

#############
# VARIABLES #
#############
# Search for computer serial number
serialNumber=`system_profiler SPHardwareDataType | grep "Serial Number (system)" | awk '{print $4}'`
# Check warranty date on JSS server
warrantyExpiresJSS=`curl -sk -u ${jssAPIUser}:${jssAPIPassword} -X GET "${jssURL}/computers/serialnumber/${serialNumber}" | awk -F '<warranty_expires>|</warranty_expires>' '{print $2}'`

###############
# APPLICATION #
###############
# Call function CheckWarranty
CheckWarranty

# -z checks if the length of the STRING is zero
if [[ -z "${warrantyExpiresJSS}" ]]; then
    echo "No warranty date is set for this computer."
    # Call function PUTCommand
    PUTCommand
else
    echo "This computer already has the warranty date."
    # Check if the warranty date in JSS is the same as the Apple one
    if [ "${warrantyExpiresJSS}" = "${warrantyExpiresApple}" ]; then
        echo "The warranty date for this computer according to Apple is $warrantyExpiresApple."
        echo "The warranty date for this computer according to JSS is $warrantyExpiresJSS."
    else
        echo "The warranty date for this computer according to Apple $warrantyExpiresApple is different from the on JSS $warrantyExpiresJSS."
        echo "Fixing it..."
        # Call function PUTCommand
        PUTCommand
    fi
fi

# Delete $outputFile from system
rm -rf ${outputFile}


exit 0

I hope it can help you too in inserting this data into your system.

stevewood
Honored Contributor II
Honored Contributor II

@luispalumbo Nice script! I've been using an Extension Attribute based on this post:

Warranty Status and Expiration

Never really considered using the API to then put it into the computer record. Nice.

Thanks for sharing!

luispalumbo
Contributor

@stevewood Thanks for that, I hope it can be useful for you.

I didn't want to use this script as an Extension Attibute because, you can correct me if I'm wrong, scripts in Extension Attributes run everytime the computer updates the inventory, and if that is correct, it would generate a lot network traffic once we have about 1200 managed computers in our environment.

Cheers!

cstout
Contributor III
Contributor III

@stevewood Would you mind letting me know how I can find what xpath is able to reference? I've got an old broken EA that looks for blanks in the warranty expiration date field. I took a look at casperurl:8443/api and see the first part of of the curl command: /JSSResource/computers/serialnumber/ but I can't find anything on the xpath mentioned. I suspect that's what has changed that broke the EA. It is currently pointing to: xpath /computer/purchasing/warranty_expires[1] and results in an error. This EA has been dead since JSS 9.7, I believe.

mm2270
Legendary Contributor III

@cstout The best way I've seen to figure out what xpath "path" to use in a script is to go to your JSS' API URL, and pull up a similar record in a browser, meaning something like https://your.jss.server:8443/JSSResource/computers/name/computername
Log in with your API credentials when asked for them. Almost any browser will format the xml that is pulled in a way that you can see the path, based on the sections. Essentially anything that can be hidden or viewed with the little disclosure triangles when viewed in a browser will be part of the path that xpath needs. FWIW, I just looked on our 9.73 JSS and /computer/purchasing/warranty_expires[1] is still a valid path to the warranty_expires item, so not sure why you'd be seeing an error. I would check to make sure the credentials being used for the curl command are still valid.

cstout
Contributor III
Contributor III

@mm2270 I've spent the last 30 minutes staring into the darkness that is this simple script and there's a syntax error somewhere but it's escaping me.

#!/bin/sh

apiURL="https://casperurl.com:8443"
apiUser="username"
apiPass="password"

sn=`ioreg -l |grep IOPlatformSerialNumber | awk '{print $4}' | cut -d " -f 2`

warinf=$(curl -k -s -u $apiUser:$apiPass $apiURL/JSSResource/computers/serialnumber/$sn | xpath //computer/purchasing/warranty_expires[1] | sed 's,<warranty_expires>,,;s,</warranty_expires>,,')

warinf2=`echo $warinf | grep "-" | cut -c -4`

if ((${warinf2} >0)); then
   echo "<result>${warinf}</result>"
else
   echo "<result>N/A</result>"
fi

This has worked in the past but currently is broken. When I run the script locally to see output I can see it's hanging up on:

warinf=$(curl -k -s -u $apiUser:$apiPass $apiURL/JSSResource/computers/serialnumber/$sn | xpath //computer/purchasing/warranty_expires[1] | sed 's,<warranty_expires>,,;s,</warranty_expires>,,')

If I remove the variables from the $warinf variable ($apiUser, $apiPass, $apiURL, $sn) and input that information manually it then hangs up on:

if ((${warinf2} >0)); then

davidacland
Honored Contributor II
Honored Contributor II

Works ok for me with curl -H "Accept: text/xml" -sfku $apiUser:$apiPass

This looks like its just reading from the warranty expiration field thats already in the JSS. What's the end goal?

davidacland
Honored Contributor II
Honored Contributor II

Ignore me, just read the bit about looking for blank fields. Although couldn't you just make a smart group looking for blank fields? Do you want it populated with something automatically?

cstout
Contributor III
Contributor III

@davidacland we have such a high client count that running a gsx scan for warranty info takes about 4 hours and then you risk timeouts when actually applying the info that it found.

Running an extension attribute like this one allows us to run a warranty check against a much smaller group and takes minutes instead of hours.

We would love to use a smart group but the criteria is lacking. I believe there's a feature request for this as well. You can't currently build a smart group that looks for missing warranty info.

stevewood
Honored Contributor II
Honored Contributor II

@cstout instead of testing for the existence of data, test for a blank string in your variable:

if [[ -z ${warinf2} ]]; then
    echo "<result>N/A</result>"
else
   echo "<result>${warinf}</result>"
fi

I tested that and it came back fine. Using your statement I received the error.

cstout
Contributor III
Contributor III

@stevewood That did provide a working result but I'm still seeing something that's maybe a non-issue:

mismatched tag at line 10, column 2, byte 420:
<p>You can get technical details <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">here</a>.<br>
Please continue your visit at our <a href="/">home page</a>.
</p>
=^
</body>
</html>
 at /System/Library/Perl/Extras/5.18/darwin-thread-multi-2level/XML/Parser.pm line 187.

stevewood
Honored Contributor II
Honored Contributor II

@cstout where are you seeing that error? Are you trying to run that script from terminal, or did you put it into an EA and run a recon on a machine?

That error sounds like you're missing a tag in the XML somewhere.

cstout
Contributor III
Contributor III

@stevewood Sorry, I'm running the script locally for testing but just realized this last test was on a unmanaged client. I ran the script on a managed client and received no errors. Thank you very much!

mm2270
Legendary Contributor III

May I offer a few suggestions?
One, if all you want to pull is the purchasing/warranty info, I suggest using the subset call in the API curl command, like this

curl -sfku username:password https://your-jss-url:8443/JSSResource/computers/serialnumber/$serial/subset/purchasing

The advantage is you're not pulling down the entire xml record for the Mac, just the purchasing section, which should help speed up the one portion of the script that is probably adding time, namely the curl command.

Also, consider using xmllint instead of xpath, which can be used to format the output into valid xml structure, which I find is easier to parse in a script without necessarily needing to know the path to use with xpath. For example:

curl -sfku username:password https://your-jss-url:8443/JSSResource/computers/serialnumber/$serial/subset/purchasing | xmllint --format -

Advantage: Even if the structure to use for xpath changes, the script may continue to work since you'll be able to use grep or awk to locate the exact line with the data in it you want.

You can simply grep for a blank value with any of the tags you want to look for. In this case, since you're looking for a blank entry for warranty_expires, you can use something like this:

curl -sfku username:password https://your-jss-url:8443/JSSResource/computers/serialnumber/$serial/subset/purchasing | xmllint --format - | grep "<warranty_expires/>"

If that returns anything, it means that field is blank. If not, its probably populated. There are other, perhaps better ways of checking on this, but give that a try and see if it gets you closer to what you're trying to achieve.

cstout
Contributor III
Contributor III

@mm2270 I'd love to implement anything that is more efficient and faster but unfortunately I don't know enough to implement it properly. I imagine the logic at the end of the script will need to be modified to look for the new output from your modified curl command.

mm2270
Legendary Contributor III
#!/bin/sh

apiURL="https://casperurl.com:8443"
apiUser="username"
apiPass="password"

sn=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformSerialNumber/{print $4}')

warinfo=$(curl -sfku "${apiUser}:${apiPass}" $apiURL/JSSResource/computers/serialnumber/${sn}/subset/purchasing | xmllint --format - | awk -F'>|<' '/<warranty_expires>/{print $3}')

if [ ! -z "$warinfo" ]; then
    echo "<result>${warinfo}</result>"
else
    echo "<result>N/A</result>"
fi

cstout
Contributor III
Contributor III

@mm2270 That is much faster! Thank you so much for taking the time to write that up. It works great!

Himanshu_panwar
New Contributor

@stevewood @mm2270 I am new to API stuff and try to build my own script to unmanage multiple mac devices using the JSSID. We get a list of Macs those are offline for more than 90 days from AD and we would like to pass this info to a .csv file with JSSID of these macs. I am using below code in terminal and it worked fine however when i incorporate this in to script is gives me generic error.

curl -ku $jssAPIUsername:$jssAPIPassword https://myjssURL:8443/JSSResource/computers/id/$jssID -X PUT -H "Content-type: text/xml" -d "
<?xml version="1.0" encoding="UTF-8"?><computer><general><remote_management><managed>false</managed></remote_management></general></computer>"

i modified the script as below:

#!/bin/sh

jssAPIUsername="jssAPIUsername"
jssAPIPassword="jssAPIPassword"
jssAddress="https://myJSSurl:8443"
file="/Path/to/file"

#Verify we can read the file
data=`cat $file`
if [[ "$data" == "" ]]; then
    echo "Unable to read the file path specified"
    echo "Ensure there are no spaces and that the path is correct"
    exit 1
fi

#Find how many computers to unmanage
computerqty=`awk -F, 'END {printf "%s
", NR}' $file`
echo "Computerqty= " $computerqty
#Set a counter for the loop
counter="0"

duplicates=[]

id=$((id+1))

#Loop through the CSV and submit data to the API
while [ $counter -lt $computerqty ]
do
    counter=$[$counter+1]
    line=`echo "$data" | head -n $counter | tail -n 1`
    JSSID=`echo "$line" | awk -F , '{print $1}'`

    echo "Attempting to update computer info for $JSSID"

    apiData="<computer><general><remote_management><managed>false</managed></remote_management></general></computer>" 
    output=`curl -k -u ${jssAPIUsername}:${jssAPIPassword} -X PUT -H "Accept: text/xml" -d "<?xml version="1.0" encoding="UTF-8"?>$apiData" ${jssAddress}/JSSResource/computers/id/$jssID/subset/General`

    echo $output
    #Error Checking
    error=""
    error=`echo $output | grep "Conflict"`
    if [[ $error != "" ]]; then
        duplicates+=($JSSID)
    fi
    #Increment the ID variable for the next user
    id=$((id+1))
done

echo "The following computers could not be created:"
printf -- '%s
' "${duplicates[@]}"

exit 0

I am not sure what i am doing wrong but below is the out put i get after running this script.
attempting to update computer info for 3 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed
100 580 100 439 100 141 310 99 0:00:01 0:00:01 --:--:-- 310
<html> <head> <title>Status page</title> </head> <body style="font-family: sans-serif;"> <p style="font-size: 1.2em;font-weight: bold;margin: 1em 0px;">Not Found</p> <p>The server has not found anything matching the request URI</p> <p>You can get technical details <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">here</a>.<br> Please continue your visit at our <a href="/">home page</a>. </p> </body> </html>

stevewood
Honored Contributor II
Honored Contributor II

@Himanshu.panwar

The variable for JSSID is wrong in the curl call. You have:

 output=`curl -k -u ${jssAPIUsername}:${jssAPIPassword} -X PUT -H "Accept: text/xml" -d "<?xml version="1.0" encoding="UTF-8"?>$apiData" ${jssAddress}/JSSResource/computers/id/$jssID/subset/General`

Notice that you have $jssID where the actual variable a few lines up is JSSID.

Try fixing that.

Himanshu_panwar
New Contributor

@stevewood thanks for your response i am able to made the script to work just an additional peace i need to change "Accept:" to "Content-type:"
Thanks once again...