Create Script Extension Attribute via API?

zetaomegagon
New Contributor III

Hello,

I'm tying to batch create new Extension Attributes via API using curl. I got my xml template by pulling down an EA by GETting an EA by ID and running through xmllint.

I'm very new to using API calls. Hopefully someone can see what is going on here.

When I run my code I get:

curl: (6) Could not resolve host: POST
curl: (6) Could not resolve host:

HTTP/1.1 415 Unsupported Media Type

Accept-Ranges: bytes
Cache-Control: no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0
Content-Type: text/html;charset=UTF-8
Date: Mon, 18 Mar 2019 15:47:45 GMT
Server: Jamf Cloud Node
Set-Cookie: APBALANCEID=aws.std-pagetia12-tc-5; path=/;HttpOnly;Secure;
X-FRAME-OPTIONS: SAMEORIGIN
Content-Length: 554
Connection: keep-alive

<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;">Unsupported Media Type</p>
<p>The server is refusing to service the request because the entity of the request is in a format not supported by the requested resource for the requested method</p>
<p>You can get technical details <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.16">here</a>.<br>
Please continue your visit at our <a href="/">home page</a>.
</p>
</body>
</html>

Here is my code:

#!/bin/bash -x                                                                                                        

_password=''
_user=''
_credentials="$(printf "$_user:$_password" | iconv -t ISO-8859-1 | base64 -i -)"
_urlPrefix="https://orgname.jamfcloud.com/JSSResource"
_urlRequest="$(echo computerExtensionAttributes | tr '[A-Z]' '[a-z]')/id"
_xmlPath="${HOME}/.orgname/Scratch/JAMF/extAttributes"

attribute_names=( 'finanace' 'development'
                                'facilities' 'front_office'
                                'technology' 'faculty' 'staff'
                                'student' 'blt' 'admissions'
                                'communications' 'heads' 'humanresources'
                                'art' 'spanish' 'physed' 'pka' 'pkb' 'ka' 'kb'
                                'pa' 'pb' 'pc' 'pd' '3a' '3b' 'jua' 'jub' 'jud'
                                'juc' 'ms6' 'ms7' 'ms8' )

_count=2

for _name in "${attribute_names[@]}"; do

    touch ${_xmlPath}/${_name}-ea.xml

    printf "%s" "<?xml version="1.0" encoding="UTF-8"?>                                                               
<computer_extension_attribute>                                                                                        
  <id>${_count}</id>                                                                                                  
  <name>Group File ${_name}?</name>                                                                                   
  <description>looks for /usr/local/TPS/.${_name}</description>                                                       
  <data_type>String</data_type>                                                                                       
  <input_type>                                                                                                        
    <type>script</type>                                                                                               
    <platform>Mac</platform>                                                                                          
    <script>                                                                                                          
            #!/bin/bash                                                                                               

            id_path=/usr/local/TPS                                                                                    
            id_file=.${_name}                                                                                         

            if [[ -f "$id_path"/"$id_file" ]]; then                                                                 
              echo '<result>true</result>'                                                                            
            fi                                                                                                        
   </script>                                                                                                          
  </input_type>                                                                                                       
  <inventory_display>Extension Attributes</inventory_display>                                                         
  <recon_display>Extension Attributes</recon_display>                                                                 
</computer_extension_attribute>" > ${_xmlPath}/${_name}-ea.xml

    curl --header "Authorization: Basic ${_credentials}" 
           --header "Accept: text/xml" 
           --request POST 
           --data-binary 
           --upload-file "@${_xmlPath}/${_name}-ea.xml" "${_urlPrefix}"/"${_urlRequest}"/"${_count}"

    _count=$(($_count + 1))
done
1 ACCEPTED SOLUTION

zetaomegagon
New Contributor III

@mm2270

Ok, I figured it out...

I didn't think until I was speaking with a friend last night to add the --verbose flag to curl. The Content-Type was showing Content-Type: application/x-www-form-urlencoded, so I added another --header:

Content-Type: application/xml

Boom. See screenshots.

I think just using a GET requested 'template' extension attribute would have been fine (as it has all the encoding), but I need to test this.

Thanks for all your help @mm2270

EDIT: added new screenshots...couldn't see test in my original ones.

88a4c2296fc547abb5f663e4380a0c0d
0bd13f9b683a4a079f3a351a56e91bc9

View solution in original post

7 REPLIES 7

mm2270
Legendary Contributor III

I haven't had the chance to look over your entire script and digest it, but I see two things that immediately stand out to me.

One, if you are iterating numbers to use for the IDs for the new Extension Attributes as it loops with the _count variable, which is what it looks like, that's not the proper method. To create any new object in Jamf Pro using the API, use an ID of 0. This tells the server that it should assign a new ID to it as it creates it. The server figures out what the next ID is it can use on its own.

Two, I believe the script itself must be encoded, to convert characters that are illegal in xml to ones that are legal. I may be wrong, but when I look at any script based Extension Attributes on our Jamf Pro 10.10.1 instance, they are encoded. For example, any hard returns in the script show as &#13;, and the < and > symbols turn into &lt; and &gt; respectively. And there are others besides those two. So I have a feeling that might be necessary to accept the XML during a POST.

Unfortunately I can't guide you on that conversion, if that's necessary to do. The perl command I use for similar encoding only works on single strings, not whole files, and doesn't do the same type of encoding anyway.

zetaomegagon
New Contributor III

@mm2270

  1. You are correct. Thanks for that info.
  2. On it. Just needed to be pointed in a direction.

Thanks a bunch!

EDIT: I can just pull down some new xml. It has all the encoding, I removed it.

mm2270
Legendary Contributor III

In case you need more, I actually found a command that does the trick. Again using perl, but a different one than the one I mentioned above.

This works on a whole file. In my testing, what I did was output a script into /tmp/ and then in the same script I did this to create an encoded variable for the entire script

ENCODED_SCRIPT=$(perl -p -e 'BEGIN { use CGI qw(escapeHTML); } $_ = escapeHTML($_);' /tmp/SCRIPT_1.sh)

Obviously change /tmp/SCRIPT_1.sh to another variable or name as needed.

From there, I used that to create the XML file, like so:

cat << EOEA > /tmp/EA_1.xml
<computer_extension_attribute>
    <id>0</id>
    <name>Example EA</name>
    <description>Example</description>
    <data_type>String</data_type>
    <input_type>
        <type>script</type>
        <platform>Mac</platform>
        <script>
        ${ENCODED_SCRIPT}
        </script>
    </input_type>
    <inventory_display>Extension Attributes</inventory_display>
    <recon_display>Extension Attributes</recon_display>
</computer_extension_attribute>
EOEA

Note the ${ENCODED_SCRIPT} variable where the actual script would go.
I then used a POST command to upload it, and voila! It shows up in my Jamf Pro console correctly.

13bc341833c6411d9df3745800c1eafa

It's just an example to make sure it actually works. Would still need to be tested against more complex scripts, but I think this should work.

zetaomegagon
New Contributor III

@mm2270

A couple few questions?
1. Can I see the encoded output?
2. Can I see your curl expression (or Python expressions) for uploading to your JSS instance?

After figuring out how to install CPAN and installing the CGI module, the encoding part seems to-- maybe-- work ok, though I noticed in the raw xml from a GET request with curl, that line breaks were encoded (like you mentioned). Getting Unsupported media type error for the upload to JSS.

EDIT: I'm not getting a numerical status error, just some html telling me so.

Encode example:

#!/bin/bash

id_path=/usr/local/ORG
id_file=.ms8

if [[ -f &quot;$id_path&quot;/&quot;$id_file&quot; ]]; then
  echo &lt;result&gt;true&lt;/result&gt;
fi

What my code is doing:
- iterating over array of attribute_names
- printing the extension attribute script body to a file. attribute_names are used to make the script unique to a group
- encode the extension attribute script body file in a variable
- POST the actual extension attribute not as a file (not required, just emphasized). attribute_name is used to make the actual extension attribute unique to group

Tried:
- Accept: application/xml, Accept: text/xml
- --data, --data-binary
With curl

Code:

#!/bin/bash -x                                                                                                        

_password=''
_user=''
_credentials="$(printf "$_user:$_password" | iconv -t ISO-8859-1 | base64 -i -)"
_urlPrefix="https://orgname.jamfcloud.com/JSSResource"
_urlInclude="$(echo computerExtensionAttributes | tr '[A-Z]' '[a-z]')/id"
_xmlPath="${HOME}/.tps/Scratch/JAMF/extAttributes"


attribute_names=( 'finanace' 'development' 
                             'facilities' 'front_office' 
                             'technology' 'faculty' 'staff' 
                             'student' 'blt' 'admissions' 
                             'communications' 'heads' 'humanresources' 
                             'art' 'spanish' 'physed' 'pka' 'pkb' 'ka' 'kb' 
                             'pa' 'pb' 'pc' 'pd' '3a' '3b' 'jua' 'jub' 'jud' 
                             'juc' 'ms6' 'ms7' 'ms8' )


for _name in "${attribute_names[@]}"; do

    printf "%s
" 
"#!/bin/bash                                                                                        

 id_path=/usr/local/ORG                                                                                      
 id_file=.${_name}                                                                                                    

 if [[ -f "$id_path"/"$id_file" ]]; then                                                                        
   echo <result>true</result>                                                                                         
 fi" > ${_xmlPath}/${_name}_ea.sh

    _encoded_script=$(perl -p -e 'BEGIN { use CGI qw(escapeHTML); } $_ = escapeHTML($_);' 
                           ${_xmlPath}/${_name}_ea.sh)

    curl -X POST 
         "${_urlPrefix}"/"${_urlInclude}"/0 
         --header "Authorization: Basic ${_credentials}" 
         --header "Accept: application/xml" 
         --data  "<computer_extension_attribute>                                                                      
<id>0</id>                                                                                                            
<name>Group File ${_name}?</name>                                                                                     
<description>looks for /usr/local/TPS/.${_name}</description>                                                         
<data_type>String</data_type>                                                                                         
<input_type>                                                                                                          
<type>script</type>                                                                                                   
<platform>Mac</platform>                                                                                              
<script>                                                                                                              
${_encoded_script}                                                                                                    
</script>                                                                                                             
</input_type>                                                                                                         
<inventory_display>Extension Attributes</inventory_display>                                                           
<recon_display>Extension Attributes</recon_display>                                                                   
</computer_extension_attribute>"

done

zetaomegagon
New Contributor III

@mm2270

Ok, I figured it out...

I didn't think until I was speaking with a friend last night to add the --verbose flag to curl. The Content-Type was showing Content-Type: application/x-www-form-urlencoded, so I added another --header:

Content-Type: application/xml

Boom. See screenshots.

I think just using a GET requested 'template' extension attribute would have been fine (as it has all the encoding), but I need to test this.

Thanks for all your help @mm2270

EDIT: added new screenshots...couldn't see test in my original ones.

88a4c2296fc547abb5f663e4380a0c0d
0bd13f9b683a4a079f3a351a56e91bc9

mm2270
Legendary Contributor III

Hey @zetaomegagon An example curl command I use for uploading an xml file to the JSS would be like this:

curl -u username:password https://your.jamf.instance.com/JSSResource/computerextensionattributes/id/0 -X POST -T /tmp/file.xml

The -T flag lets you specify a file to use as the upload, but you can also use --data flag for uploading direct data as you have it. I don't often has as much luck using a direct data upload though, so I tend to use a file.

As for the encoded script, here is the one I used in the test I mentioned above

#!/bin/bash

if [ -e &quot;/Applications/Safari.app&quot; ]; then
    echo &quot;&lt;result&gt;Installed&lt;/result&gt;&quot;
else
    echo &quot;&lt;result&gt;Not Installed&lt;/result&gt;&quot;
fi

Interestingly, the encoding here looks a bit different than the way it would appear in the Jamf Pro server, but, despite the differences, it accepted the XML and worked to create the EA for me. I guess there's no hard rule on the encoding, as long as it's something that is valid for inclusion in an XML file.

Hope the above helps.

zetaomegagon
New Contributor III

@mm2270

Cool. That is basically how my encoding looks.

At least for curl 7.61.1, you can use --data or --data-binary and reference a file like this: @/path/to/file. I had been using the -T option as --upload-file. I like to use long options if possible, for reference, because I will have to train other members of my staff to use cli/shell in the future.

Please note: in my original script, I had tried about half a dozen options and some jenky stuff made it into the script (like: --data-binary followed by --upload-file). Original script should be considered alpha quality.

EDIT: Ah. I see --data @/path/to/file will just read the contents, where as -T|--upload-file will actually upload the file. My bad.