Python Script Error

blarwick
New Contributor

I'm fairly new to JAMF, but wrote a Python script that I want to call using a Self-Service Policy.

The simple point of the script is that it allows a user to enter in a description of a problem they are having with the computer, optionally add a screenshot, then email that information (along with their username/computer name/serial number) to an email address/ticketing system.

The script works perfectly when executed locally. When executed through a JAMF policy on the same computer, it does not appear to run and I get a Completed status in JAMF with the following error:

Executing Policy Request IT Support…
[Step 1 of 2]
Mounting Production (Master) to /Volumes/CasperShare…

[Step 2 of 2]
Running script EmailSupportTicket.py…
Script exit code: 127
Script result: /bin/sh: /Library/Application Support/JAMF/tmp/EmailSupportTicket.py: No such file or directory

-----
Script follows:

#!/usr/bin/python

import smtplib, email, sys, time, os
from Tkinter import
from ctypes import

from ctypes import util
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders

iokit = cdll.LoadLibrary(util.find_library('IOKit'))
cf = cdll.LoadLibrary(util.find_library('CoreFoundation'))

cf.CFStringCreateWithCString.argtypes = [c_void_p, c_char_p, c_int32]
cf.CFStringCreateWithCString.restype = c_void_p
cf.CFStringGetCStringPtr.argtypes = [c_void_p, c_uint32]
cf.CFStringGetCStringPtr.restype = c_char_p

kCFAllocatorDefault = c_void_p.in_dll(cf, "kCFAllocatorDefault")
kCFStringEncodingMacRoman = 0

kIOMasterPortDefault = c_void_p.in_dll(iokit, "kIOMasterPortDefault")
kIOPlatformSerialNumberKey = "IOPlatformSerialNumber".encode("mac_roman")
iokit.IOServiceMatching.restype = c_void_p
iokit.IOServiceGetMatchingService.argtypes = [c_void_p, c_void_p]
iokit.IOServiceGetMatchingService.restype = c_void_p
iokit.IORegistryEntryCreateCFProperty.argtypes = [c_void_p, c_void_p, c_void_p, c_uint32]
iokit.IORegistryEntryCreateCFProperty.restype = c_void_p
iokit.IOObjectRelease.argtypes = [c_void_p]

SERIAL = None
def getSerial(): global SERIAL if SERIAL is None: platformExpert = iokit.IOServiceGetMatchingService(kIOMasterPortDefault, iokit.IOServiceMatching("IOPlatformExpertDevice")) if platformExpert: key = cf.CFStringCreateWithCString(kCFAllocatorDefault, kIOPlatformSerialNumberKey, kCFStringEncodingMacRoman) serialNumberAsCFString = iokit.IORegistryEntryCreateCFProperty(platformExpert, key, kCFAllocatorDefault, 0); if serialNumberAsCFString: SERIAL = cf.CFStringGetCStringPtr(serialNumberAsCFString, 0)

iokit.IOObjectRelease(platformExpert)

return SERIAL

# set the variables
sender = str(sys.argv[3]) + "<removed - actual script has our domain information>"

# identify the correct changegear queue
receiver = str(sys.argv[4])

subj = "Service Request from " + str(sys.argv[3])
smtpHost = "<removed for security reasons - the actual script has this server information>"
port = 587

# locate serial number
serial_number = getSerial();

# bodyText initial information
bodyText = " Username: " + str(sys.argv[3]) + " Computer Name: " + str(sys.argv[2]) + " Serial Number: " + serial_number + " "

# setup the message header
timegmt = time.gmtime(time.time( ))
fmt = '%a, %d %b %Y %H:%M:%S GMT'
datestr = time.strftime(fmt, timegmt)
msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = receiver
msg['Date'] = datestr
msg['Subject'] = subj

# request bodyText
root = Tk()
screenshot_yes = IntVar()

def sendrequest(): msg.attach( MIMEText(bodyText + T.get('1.0', 'end')) ) root.destroy() if screenshot_yes.get() == 1: os.system("screencapture /tmp/screen.png") part = MIMEBase('application', "octet-stream") part.set_payload( open("/tmp/screen.png","rb").read() ) Encoders.encode_base64(part) part.add_header('Content-Disposition', 'attachment; filename="screenshot.png"') msg.attach(part) server = smtplib.SMTP(smtpHost) failed = server.sendmail(sender, receiver, msg.as_string()) server.quit()

# return the status if failed: mailsent = Tk() mailsent.title("Error") label = Message(mailsent, text="Unable to send. You must be on campus or connected to the VPN to use this function.", width=600) label.pack() FB = Button(mailsent, text="OK", command=mailsent.destroy) FB.pack(side=RIGHT) mailsent.mainloop() else: mailsent = Tk() mailsent.title("Success") if screenshot_yes.get() == 1: label = Message(mailsent, text="Your request for assistance has been sent with a screenshot of your desktop.", width=300) else: label = Message(mailsent, text="Your request for assistance has been sent.", width=300) label.pack() FB = Button(mailsent, text="OK", command=mailsent.destroy) FB.pack(side=RIGHT) mailsent.mainloop()

root.title("Request IT Assistance")
S = Scrollbar(root)
L = Message(root, text="Please describe the problem you are having below, then click the Send Email button. To include a screenshot of your current desktop, select the checkbox below.", width=800)
L.pack(side=TOP)
T = Text(root, height=20, width=80)
T.focus()
S.pack(side=RIGHT, fill=Y)
T.pack(side=TOP, fill=Y)
S.config(command=T.yview)
T.config(yscrollcommand=S.set)
b2 = Button(root, text="Send Email to " + receiver, command=sendrequest)
b2.pack(side=RIGHT)
b1 = Button(root, text="Cancel", command=root.destroy)
b1.pack(side=RIGHT)
screenshot_yes.set(0)
c = Checkbutton(root, text="Send Screenshot", variable=screenshot_yes)
c.pack(side=LEFT)
root.mainloop()

8 REPLIES 8

mm2270
Legendary Contributor III

I don't know Python, but I suspect its not an issue with your script. Don't be fooled by the "Running script" line. It always says that in the logs because it assumed the download of the script was successful in the previous step. The error you're seeing usually means it was unable to download the script from the Distribution Point or the JSS. This could be because of permissions issues, or the script isn't actually on the distribution point in question (assuming its not on JSS 9.x and in the database)

What I'd try is running the policy by its JSS ID on a Mac from Terminal and throw the -verbose flag into it.
Something like

sudo jamf policy -id <id> -verbose

It may show in the verbose output that its not actually able to find or pull the script down.

lg-jbarclay
New Contributor II

I suspect @mm2270][/url is correct.

Also, is there any reason why you're using ctypes to import the CoreFoundation library?

As long as you're invoking system Python, and as long as your users haven't messed with it, you can do this:

import CoreFoundation as cf

...instead of:

cf = cdll.LoadLibrary(util.find_library('CoreFoundation'))

blarwick
New Contributor

sudo jamf policy -id 503 -verbose verbose: Checking for an existing instance of this application...
Checking for policy ID 503... verbose: Checking for active connection on interface "Ethernet"... verbose: No active connection on "Ethernet"... verbose: The Management Framework Settings are up to date. verbose: Found 1 matching policies. verbose: Removing any cached policies for this trigger. verbose: Parsing servers... verbose: Parsing Policy Request IT Support (503)... verbose: Parsing Policy Request IT Support (503)...
Executing Policy Request IT Support...
Mounting Production (Master) (jss-servername-here) to /Volumes/CasperShare... verbose: Result of mount attempt: Password: verbose: Result code of mount attempt: 0 verbose: Copying script to temp directory... verbose: Determining script type...
Running script las-EmailSupportTicket.py...
Script exit code: 127
Script result: /bin/sh: /Library/Application Support/JAMF/tmp/las-EmailSupportTicket.py: No such file or directory

verbose: Removing local copy...
Submitting log to https://jss-servername-here:8443/
Unmounting file server…

@lg-jbarclay - That portion of the code is copied from another writer. I haven't gone through and cleaned it up.

mm2270
Legendary Contributor III

Hmm, seems like from the verbose output its downloading the script OK, but I'm not sure about this line-
Script result: /bin/sh: …
Seems like its trying to run the script as a bash script, and not respecting that its a python script? I don't have any python scripts in our JSS I can test against, so I don't know if the /bin/sh always shows up that way or not. But I did a quick search because the shebang line in your script looked different than what I recall seeing in other python scripts. Can you try changing it to #!/usr/bin/env python as per this link?
http://stackoverflow.com/questions/17846908/proper-shebang-for-python-script

See if it works after that maybe?

blarwick
New Contributor

The #!/usr/bin/env python is what I used in my original script. I switched it to the specific python path in hopes that that might be the problem. The version you see above was that fix attempt that did nothing.

In either case, it doesn't seem to call it.

And yes, that is the same line that bothers me as well. Not sure why it lists a bash script processor and then lists the path to a python script. Seems like it shouldn't do that, but this is my first experience with JAMF and am not even sure what to tell the administrators of the system if that is indeed the problem.

mm2270
Legendary Contributor III

I'm also not clear if it should be doing that. My understanding is, proper procedure should be to read the script interpreter from the shebang line and not assume anything. Beyond python scripts, it could also run afoul with some bash scripts. Some bash specific stuff, like process substitution, isn't valid in a Bourne shell script (/bin/sh) and will only work when run as /bin/bash, just as an example. I hope that its not making assumptions. I would check in with JAMF support on that though. Seems odd it would show a bash interpreter as the first part of the run script line in the log.

tron_jones
Release Candidate Programs Tester

I just ran your script from Self Service and it worked fine using the #!/usr/bin/env python. I don't know how your original script looks since it was posted in non indentation format. Here's the version that worked from Self Service that I indented in Text Wrangler. I'm thinking it wouldn't run because of an indentation error within the script. I didn't go over the whole script but just enough to get it to launch.

#!/usr/bin/env python


import smtplib, email, sys, time, os
from Tkinter import *
from ctypes import *
from ctypes import util
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders

iokit = cdll.LoadLibrary(util.find_library('IOKit'))
cf = cdll.LoadLibrary(util.find_library('CoreFoundation'))

cf.CFStringCreateWithCString.argtypes = [c_void_p, c_char_p, c_int32]
cf.CFStringCreateWithCString.restype = c_void_p
cf.CFStringGetCStringPtr.argtypes = [c_void_p, c_uint32]
cf.CFStringGetCStringPtr.restype = c_char_p

kCFAllocatorDefault = c_void_p.in_dll(cf, "kCFAllocatorDefault")
kCFStringEncodingMacRoman = 0

kIOMasterPortDefault = c_void_p.in_dll(iokit, "kIOMasterPortDefault")
kIOPlatformSerialNumberKey = "IOPlatformSerialNumber".encode("mac_roman")
iokit.IOServiceMatching.restype = c_void_p
iokit.IOServiceGetMatchingService.argtypes = [c_void_p, c_void_p]
iokit.IOServiceGetMatchingService.restype = c_void_p
iokit.IORegistryEntryCreateCFProperty.argtypes = [c_void_p, c_void_p, c_void_p, c_uint32]
iokit.IORegistryEntryCreateCFProperty.restype = c_void_p
iokit.IOObjectRelease.argtypes = [c_void_p]

SERIAL = None
def getSerial():
    global SERIAL
    if SERIAL is None:
        platformExpert = iokit.IOServiceGetMatchingService(kIOMasterPortDefault,
        iokit.IOServiceMatching("IOPlatformExpertDevice"))
    if platformExpert:
        key = cf.CFStringCreateWithCString(kCFAllocatorDefault, kIOPlatformSerialNumberKey, kCFStringEncodingMacRoman)
        serialNumberAsCFString = 
        iokit.IORegistryEntryCreateCFProperty(platformExpert, key, kCFAllocatorDefault, 0);
    if serialNumberAsCFString:
        SERIAL = cf.CFStringGetCStringPtr(serialNumberAsCFString, 0)
        iokit.IOObjectRelease(platformExpert)
        return SERIAL

# set the variables
sender = str(sys.argv[3]) + "<removed - actual script has our domain information>"

# identify the correct changegear queue
receiver = str(sys.argv[4])

subj = "Service Request from " + str(sys.argv[3])
smtpHost = "<removed for security reasons - the actual script has this server information>"
port = 587

# locate serial number
serial_number = getSerial();

# bodyText initial information
bodyText = "
Username: " + str(sys.argv[3]) + "
Computer Name: " + str(sys.argv[2]) + "
Serial Number: " + serial_number + "

"

# setup the message header
timegmt = time.gmtime(time.time( ))
fmt = '%a, %d %b %Y %H:%M:%S GMT'
datestr = time.strftime(fmt, timegmt)
msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = receiver
msg['Date'] = datestr
msg['Subject'] = subj

# request bodyText
root = Tk()
screenshot_yes = IntVar()

def sendrequest():
    msg.attach( MIMEText(bodyText + T.get('1.0', 'end')) )
    root.destroy()
    if screenshot_yes.get() == 1:
        os.system("screencapture /tmp/screen.png")
        part = MIMEBase('application', "octet-stream")
        part.set_payload( open("/tmp/screen.png","rb").read() )
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="screenshot.png"')
        msg.attach(part)
        server = smtplib.SMTP(smtpHost)
        failed = server.sendmail(sender, receiver, msg.as_string())
        server.quit() 

# return the status
    if failed:
        mailsent = Tk()
        mailsent.title("Error")
        label = Message(mailsent, text="Unable to send.

You must be on campus or connected to the VPN to use this function.", width=600)
        label.pack()
        FB = Button(mailsent, text="OK", command=mailsent.destroy)
        FB.pack(side=RIGHT)
        mailsent.mainloop()
    else:
        mailsent = Tk()
        mailsent.title("Success")
    if screenshot_yes.get() == 1:
        label = Message(mailsent, text="Your request for assistance has been sent with a screenshot of your desktop.", width=300)
    else:
        label = Message(mailsent, text="Your request for assistance has been sent.", width=300)
        label.pack()
        FB = Button(mailsent, text="OK", command=mailsent.destroy)
        FB.pack(side=RIGHT)
        mailsent.mainloop()

root.title("Request IT Assistance")
S = Scrollbar(root)
L = Message(root, text="Please describe the problem you are having below, then click the Send Email button.
To include a screenshot of your current desktop, select the checkbox below.", width=800)
L.pack(side=TOP)
T = Text(root, height=20, width=80)
T.focus()
S.pack(side=RIGHT, fill=Y)
T.pack(side=TOP, fill=Y)
S.config(command=T.yview)
T.config(yscrollcommand=S.set)
b2 = Button(root, text="Send Email to " + receiver, command=sendrequest)
b2.pack(side=RIGHT)
b1 = Button(root, text="Cancel", command=root.destroy)
b1.pack(side=RIGHT)
screenshot_yes.set(0)
c = Checkbutton(root, text="Send Screenshot", variable=screenshot_yes)
c.pack(side=LEFT)
root.mainloop()

bmodesitt
New Contributor II

I ran into this problem, and fixed it by running python scripts within a bash script..

#!/bin/bash
/usr/bin/env python <<EOF
# python script goes here
EOF