Install apps automatically OR allow via self service

oschools20
New Contributor

solved

1 REPLY 1

jared_f
Valued Contributor

Hi,

What we decided to do was to delete the app store because the people we manage have a habit of installing games and we made everything downloadable via Self Service. The thing I like about Self Service is that I don't dictate what is on "their" iPads. We have around 40 apps in Self Service (using VPP) and if the they only uses 5, it is a waste of space to install 40 apps on their device. We used a script that we found on JAMF nation that leverages the JSS API and creates static mobile device groups based on class enrollment (from a CSV file- maybe from your student information system?). It is very handy.

All in all, I would delete the app store, we tried to keep it on the iPad for years but, they didn't have enough self control. Self Service has been wonderful. We recommend using the web clip because it is much easier than having the app on the device.

Good Luck!

#!/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()

SOURCE: Click Here