Posted on 10-02-2012 09:08 AM
We began a 1:1 iPad program this year. One of our biggest hurdles was scoping apps, ebooks, and configuration profiles based on a student’s enrollment in a particular class. We initially attempted to create smart groups based off of configuration profiles that we made available in Self Service. Not only did this take a considerable amount of time to create, but it also slowed down loading Self Service substantially.
Fortunately, we came across this Python script and were able to have it adapted for our needs. Our student information system was able to export a CSV file in the format Username,Class. This Python script is able to take the CSV file and leverage the JSS API. It creates groups for the classes. It then compares the student’s username to the inventory of devices and then enrolls that device in a static group for each of his classes. The current limitations of the script are that it will only recognize one device per user and that the class names cannot contain a + or a /. We then used the Static Groups to scope apps and ebooks.
Hopefully some of you with similar 1:1 deployments will find it useful. I can and will answer any questions to the best of my ability, but I did not write this script and do not have the ability at this time to assist in adapting it to different student information system outputs.
#!/usr/bin/env python
# Limitations:
# - Only one device per student is supported
# - Classes may not contain a + or / character as the API cannot parse these at this time
# Settings
jssAddr = "https://casper.yourcompany.org:8443" # JSS server address with or without https:// and port
jssUser = "JSSLogin" # JSS login username (API privileges must be set)
jssPass = "Password" # JSS login password
csvPath = "/users/Admin/desktop/jssscript/StudentsByCourse.csv" # Path and file name for the input CSV file
# Imports
from urllib2 import urlopen, URLError, HTTPError, Request
from xml.dom import minidom
from xml.sax import make_parser, ContentHandler
from sys import exc_info, argv, stdout, stdin
from getpass import getpass
import base64
import csv
import re
def main():
global jssAddr
global jssUser
global jssPass
global csvPath
updateMDGroups = JSSUpdateMobileDeviceGroups()
studentList = updateMDGroups.getCSVData(csvPath)
updateMDGroups.updateGroups(jssAddr, jssUser, jssPass, studentList)
class JSSUpdateMobileDeviceGroups:
def __init__(self):
self.numMobileDevicesUpdated = 0
self.deviceMap = dict()
def getCSVData(self, csvPath):
# Read in CSV
csvinput = open(csvPath)
reader = csv.reader(csvinput)
return reader
def updateGroups(self, jssAddr, jssUser, jssPass, studentList):
# Cache mobile device ID to student username mapping
self.grabMobileDeviceData(jssAddr, jssUser, jssPass)
# Assign student device to the classes (groups)
url = jssAddr + "/JSSResource/mobiledevicegroups"
grabber = CasperGroupPUTHandler(jssUser, jssPass)
for studentLine in studentList:
if studentLine[0] != "Student ID":
self.handleStudentDeviceAssignment(grabber, url, studentLine)
print "Successfully updated %d devices in the JSS." % self.numMobileDevicesUpdated
def grabMobileDeviceData(self, jssAddr, jssUser, jssPass):
url = jssAddr + "/JSSResource/mobiledevices"
grabber = CasperDeviceXMLGrabber(jssUser, jssPass)
grabber.parseXML(url, CasperMobileDeviceListHandler(grabber, self.deviceMap))
def handleStudentDeviceAssignment(self, grabber, url, studentLine):
# Create mobile device studentLine XML...
if studentLine[0] in self.deviceMap:
if "/" in studentLine[1] or "+" in studentLine[1]:
print "Error: User: %s, Class: %s, Error: Class contains forward slash or ends in plus character" % (studentLine[0], studentLine[1])
else:
studentDeviceID = self.deviceMap[studentLine[0]]
newGroupAssignment = self.createNewGroupElement(studentDeviceID, studentLine[1])
self.handleGroupPUT(grabber, url, studentLine[1], newGroupAssignment)
else:
print "Error: User: %s, Class: %s, Error: Could not find a mobile device match for student username" % (studentLine[0], studentLine[1])
def handleGroupPUT(self, grabber, url, className, newGroupAssignment):
# PUT new XML
apiClassURLRAW = url + "/name/" + className
apiClassURL = apiClassURLRAW.replace (' ', '+')
apiClassName = className.replace ('+', '')
apiClassName = apiClassName.replace (' ', '+')
###########UNCOMMENT NEXT TWO LINES FOR DEBUG MODE#############
#print "PUT-ing URL %s: " % (apiClassURL)
#print newGroupAssignment.toprettyxml()
putStatus = grabber.openXMLStream("%s/name/%s" % (url, apiClassName), newGroupAssignment)
if putStatus is None:
self.numMobileDevicesUpdated += 1
def createNewGroupElement(self, studentDeviceID, groupName):
global eventValues
newGroupAssignment = minidom.Document()
group = self.appendEmptyElement(newGroupAssignment, newGroupAssignment, "mobile_device_group")
self.appendNewTextElement(newGroupAssignment, group, "name", groupName)
groupAdditions = self.appendEmptyElement(newGroupAssignment, group, "mobile_device_additions")
deviceElement = self.appendEmptyElement(newGroupAssignment, groupAdditions, "mobile_device")
self.appendNewTextElement(newGroupAssignment, deviceElement, "id", studentDeviceID)
return newGroupAssignment
def appendEmptyElement(self, doc, section, newElementTag):
newElement = doc.createElement(newElementTag)
section.appendChild(newElement)
return newElement
def appendNewTextElement(self, doc, section, newElementTag, newElementValue):
newElement = self.appendEmptyElement(doc, section, newElementTag)
newValueElement = doc.createTextNode(newElementValue)
newElement.appendChild(newValueElement)
return newElement
class CasperDeviceXMLGrabber:
def __init__(self, jssUser, jssPass):
self.jssUser = jssUser
self.jssPass = jssPass
def parseXML(self, url, handler):
req = Request(url)
base64string = base64.encodestring('%s:%s' % (self.jssUser, self.jssPass))[:-1]
authheader = "Basic %s" % base64string
req.add_header("Authorization", authheader)
try:
MobileDeviceList = urlopen(req)
except (URLError, HTTPError) as urlError: # Catch errors related to urlopen()
print "Error when opening URL: " + urlError.__str__()
exit(1)
except: # Catch any unexpected problems and bail out of the program
print "Unexpected error:", exc_info()[0]
exit(1)
parser = make_parser()
parser.setContentHandler(handler)
parser.parse(MobileDeviceList)
class CasperGroupPUTHandler:
def __init__(self, jssUser, jssPass):
self.jssUser = jssUser
self.jssPass = jssPass
def parseXML(self, url):
return self.openXMLStream(url, None)
def openXMLStream(self, url, xmlout):
try:
if xmlout is None:
req = Request(url)
else:
req = Request(url, data=xmlout.toxml())
req.add_header('Content-Type', 'text/xml')
req.get_method = lambda: 'PUT'
base64string = base64.encodestring('%s:%s' % (self.jssUser, self.jssPass))[:-1]
authheader = "Basic %s" % base64string
req.add_header("Authorization", authheader)
xmldata = urlopen(req)
if xmlout is None:
xmldoc = minidom.parse(xmldata)
else:
xmldoc = None
return xmldoc
except (URLError, HTTPError) as urlError: # Catch errors related to urlopen()
if urlError.code == 404:
if xmlout is None:
req = Request(url)
else:
req = Request(url, data=xmlout.toxml())
req.add_header('Content-Type', 'text/xml')
req.get_method = lambda: 'POST'
base64string = base64.encodestring('%s:%s' % (self.jssUser, self.jssPass))[:-1]
authheader = "Basic %s" % base64string
req.add_header("Authorization", authheader)
xmldata = urlopen(req)
if xmlout is None:
xmldoc = minidom.parse(xmldata)
else:
xmldoc = None
return xmldoc
print "Error when opening URL %s - %s " % (url, urlError.__str__())
return "Error"
except: # Catch any unexpected problems and bail out of the program
print "Unexpected error:", exc_info()[0]
exit(1)
# This class is used to parse the /mobiledevices list to get all of the ids and usernames
class CasperMobileDeviceListHandler(ContentHandler):
def __init__(self, grabber, deviceMap):
ContentHandler.__init__(self)
self.grabber = grabber
self.deviceMap = deviceMap
self.currentID = ""
self.inID = False
self.inSize = False
self.inUsername = False
def startElement(self, tag, attributes):
if tag == "id":
self.inID = True
elif tag == "username":
self.inUsername = True
elif tag == "size":
self.inSize = True
def endElement(self, tag):
self.inID = False
self.inSize = False
self.inUsername = False
if tag == "mobiledevices":
print "Finished collecting mobile devices for lookup"
def characters(self, data):
if self.inID:
self.currentID = data
elif self.inUsername:
if data != "":
self.deviceMap[data] = self.currentID
elif self.inSize:
self.numDevices = data
print "Collecting data for " + data + " Mobile Device(s)..."
if __name__ == "__main__":
main()
Posted on 07-19-2016 03:04 PM
Can you post what your csv file looks like. I'm just wanting to make sure I have the format right.
Posted on 06-04-2017 05:59 PM
Does this still work?
Posted on 06-05-2017 05:35 AM
It does. I use it to populate some static groups, but for class enrollments and for scoping books to classes I actually just use the SIS Importer now.
Posted on 08-20-2017 01:22 PM
Can this be changed to create static user groups instead of static device groups?
Posted on 08-21-2017 07:03 AM
It cannot. This was a necessary tool before Jamf developed the SIS importer tool. But luckily @franton was exceedingly kind and has a resource for us all on his GitHub: https://github.com/franton/Add-JSS-User-to-Group