I'm looking for some guidance and best practice ideas. We are a district of about 1000 macs and 200 iPads. The macs we manage with success but we are struggling with how to best distribute apps to iPads. We have a mix of uses - some carts, personally assigned to students, and others devoted to classrooms. Because we don't have a 1:1, having a fluid system for app curation isn't really in place. We struggle with how to best distribute apps. The biggest issue is not having to choose between management and spontaneity. Right now if a teacher even wants a free app:
We've had some struggles with push not working well and needing to a bit of "Send Blank Push" to get things to go.
Am I missing anything that could make this easier? I know I haven't provided all our details but I appreciate any thoughts or other ideas anyone has to simplify.
We found it best to do everything Self Service. A lot of users ignore push notifications (i.e. casper.yourcompany.com wants to install an app). Put every app in Self Service on all devices and the users can download the app if needed or use it if it is already on the device.
Yes that works great for free apps or when they are needed on demand. It doesn't work well when we have paid apps in small quantities or you need no user interaction (especially in special needs circumstances). Just curious to hear if I others have similar or different interactions.
I recently setup two areas with a small number of iPads that get checked out to students. Apple seems to recommend Configurator 2 for something like this but I found DEP and device assigned apps to work best. I have the apps configured to auto-install. It's a slower process at the moment than just zapping each iPad with a usb cable and Configurator but this allows for three things...
Not sure if any of that helps but previous to device assigned applications self service was the most reliable but now we can set it to install automatically.
@bapettit You are doing everything as best as you can. I don't quite know why you say Self Service "doesn't work well when we have paid apps in small quantities" - whether SS or Auto Install you are still scoping to a small number of devices.
The big issue I have with Auto Installed apps is Failed Commands, especially the one about app being scheduled for management. The app will not try to install again until that error is cleared and an Update Inventory is sent. When the user requests the app from Self Service, JAMF has coded it to clear the error and try again.
Also, if you know that the VPP Managed Licenses are ready you don't have to wait for Casper to add the app to your Catalog. You can add it yourself and it'll see your licenses.
@jhuls Thank you for sharing your set up. I think you are right that a caching server should increase the likelihood of apps making it to their destination.
@cdenesha my struggles with paid apps in self-service center around a lot of high-need students with many apps that are quite expensive and similar and making sure the wrong scope doesn't swallow up a license/install. It's not a hit on self-service as much as management of one-off systems. I like the idea of not waiting on Casper to add the app, my only hesitation in the past was making sure I actually chose the right app if I'm adding it manually. It's not as easy when you are doing 30 different apps that are all named things like "123 Math," "Math 123," "counting math" and "123 counting." :) Thank you for your thoughts!
@cdenesha in my case all our students are special needs and vary greatly so we use a large number of apps but buying licences to cover each student would be very expensive. So just putting everything in self service wont work because JSS assigns a licence to every device that can install the app, not just those that have it installed. Scoping each app to each student individually would be very time consuming and defeat our aim of developing self empowerment and too slow to give the teachers the ability to decide what best fits their student's needs at the time its needed.
The third post in https://www.jamf.com/jamf-nation/feature-requests/4813/assign-device-based-mac-app-licenses-on-demand details a very manual method we use to make all of our mobile device apps available in self service.
@Graeme I understand where you are coming from... it takes a long time to scope apps to specific users. We came across a script which takes student info from you student info system and scopes apps and books based on class enrollment. We don't use it so I can't sure if it works but, it may be an option to make sure the right kids have what they need.
#!/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 != "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 in self.deviceMap: if "/" in studentLine or "+" in studentLine: print "Error: User: %s, Class: %s, Error: Class contains forward slash or ends in plus character" % (studentLine, studentLine) else: studentDeviceID = self.deviceMap[studentLine] newGroupAssignment = self.createNewGroupElement(studentDeviceID, studentLine) self.handleGroupPUT(grabber, url, studentLine, newGroupAssignment) else: print "Error: User: %s, Class: %s, Error: Could not find a mobile device match for student username" % (studentLine, studentLine) 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() 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() 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()
More Info > https://www.jamf.com/jamf-nation/discussions/5505/static-groups-by-class-enrollment
Thanks jarad_f and cdenesha. The script is interesting and we already have one running that makes the classes for Casper Focus but haven't ventured into assigning apps by users. The same script does assign the device to a specific user and sets the device's name appropriately so a transition should be easy.