Posted on 10-10-2016 06:41 AM
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.
Thanks!
Brad
Posted on 10-10-2016 11:39 AM
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.
Posted on 10-11-2016 08:39 AM
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.
Posted on 10-11-2016 09:17 AM
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.
Posted on 10-11-2016 07:51 PM
@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.
Posted on 10-12-2016 05:38 AM
@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!
Posted on 10-12-2016 07:11 PM
@bapettit You can always pull back licenses from the devices.
Posted on 10-12-2016 07:45 PM
@bapettit i recently discovered that when you search for an app to add to the catalog, you are next given a results page with three tabs. The third one shows just the matches to your VPP apps!
Posted on 10-17-2016 09:44 AM
@cdenesha that's a great tip, I wasn't aware of that!
@jared_f yes, being able to pull licenses does help. In practice that sometimes takes a bit of time or requires some poking. Since we rarely ever see these devices once they are in other's hands it is hard to know their current state.
Posted on 10-17-2016 07:07 PM
@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.
Posted on 10-19-2016 05:33 PM
@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[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()
More Info > https://www.jamf.com/jamf-nation/discussions/5505/static-groups-by-class-enrollment
Posted on 10-19-2016 09:04 PM
I just posted what we are doing with expensive one-off apps at the link Graeme posted above (clickable link).
Posted on 10-20-2016 02:30 PM
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.