install software based off of a selectable script

zmbarker
Contributor

At this time when a computer gets enrolled into my JSS, an Enrollment policy starts which deploys CocaDialog onto the device and then I have a "Set ComputerName" cocoaDialog script run that prompts for the user/tech to enter in the computername of the computer. NOTE: we have the computername change when the computer is enrolled into JSS.

Now what I would like is a solution that will allow the end user to select what software they can install once the computer is enrolled into JSS. Simple selection of a list of software with checkboxes next to the name which indicates to install a particular software.

I have been relying on the user/tech to install the apps by selecting the self-service pkg's, but this seems to be very difficult for them to accomplish, aka "they forget" even when self-service starts automatically. So instead I would like a pop-up selectable script or even better to have the software installed if the computer doesn't have the particular version.

12 REPLIES 12

mm2270
Legendary Contributor III

That's interesting. I've actually been working on something a lot like this recently, also utilizing cocoaDialog. My end goal is a little different than yours in that I was looking for a way to prompt users about available Self Service installs and then allow them to check the items they want to install and away it goes, running through each installation. My inspiration was something like Munki's Managed Software Update.app. Although cocoaDialog is obviously nowhere near as polished as a cocoa app like Managed Software Update, short of implementing Munki in our setup, this would be the next logical thing to try.
I have it working pretty well so far. The part that is very tricky though is knowing which installs are actually available to a client, i.e, what policies/checkboxes to display. For a newly imaged Mac it isn't too difficult to figure out, but for an existing system, the only real way is to build a lot of Smart Groups that in turn would be used within separate policies, or better yet, somehow feed the script. Either that or rely only on cached software items, which would be easy to direct the script to for a list of items for the resulting checkboxes. The Smart Groups route seems like a lot of overhead, so I'm not really willing to do it that way.
We're still on version 8.x of Casper Suite for the time being, and in that series, the API has no facility for a Mac to figure out what pending policies it may have. Its possible in version 9 this may be an available item in the API, but as I don't have a version 9 JSS up and running currently I can't be sure. What I'm referring to specifically is the note from JAMF in this Feature Request: https://jamfnation.jamfsoftware.com/featureRequest.html?id=111

While I can't share my script just yet, I can tell you that what I'm working on makes use of both Casper Suite script parameters ($4 thru $11) and makes heavy use of building bash arrays to feed the checkbox list dynamically to the script as well as each items policy ID or a manual trigger. The goal was to make it as flexible as possible, so a single script could be used to display anywhere from 1 checkbox to up to 8, and have it correctly parse the selections out to the appropriate installations.
In your case, it might be easier to hard code in the list of installation items and their triggers/IDs, etc that will create the checkbox list, since it sounds like this would be mostly for newly enrolled/imaged Macs.

One thing to note about cocoaDialog's checkbox function is that its buggy. Under OS X 10.7 and up the text description display doesn't work, even though the help states it as an optional flag. You get checkboxes fine, but no descriptive text above them. So I've had to call CD twice in my script (one for a 'message box' dialog pushed to the background, and the second for the checkboxes) and use its pixel level placement options to align them closely in position and size so they almost appear as one dialog. Not ideal, as I wish it could be one dialog, but its the best I can come up with as a workaround.

Anyway, since I've been going down this road, let me know how I can help if you decide you want to build something to do this.

acdesigntech
Contributor II

Yes, if you are just looking to prompt a user with a list of software they can install, you can easily populate a bash array with all of the software display names and their corresponding package names, then feed the display names into cocoaDialog, read the selections, and then use a jamf -install command to push the software down, or you can cache the packages for install on reboot. You can even populate the array dynamically when the script runs, so you can easily add/remove software titles from a centrally maintained file.

@mm2270][/url][/url, that stinks that you can't get CD to actually work with it's checkboxes on 10.7+. I was unaware of that. Would it be possible to feed an AS selection box with the basH parameters? I think You'd have to make sure you call AS in the context of whoever's going to be interacting with the dialog, ala

finder_pid=$(ps -xawwo pid,command | grep "/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder$" | awk '{print $1}')

 ## Execute the osascript, specifying the bootstrap context that we captured above
    /bin/launchctl bsexec $finder_pid /usr/bin/osascript -e 'tell application "System Events" to log out'

I still have hit or miss results with this method though.

mm2270
Legendary Contributor III

@acdesigntech][/url][/url - well, to be more clear on the bug., the checkboxes and their individual text items show up in the dialog just fine, but a standard dialog text string above all the checkboxes does not show up. If you look at the help on CD with the last beta version it states you should be able to do something like:

path/to/cocoaDialog.app/Contents/MacOS/cocoaDialog checkbox --text "Make a selection" --items "One" "Two "Three" --button1 "OK" --button2 "Cancel"

and it should show a dialog that looks like:

Make a selection ? One ? Two ? Three

Unfortunately all you get is:

? One ? Two ? Three

I haven't tried using AppleScript to do this, but quite frankly I try to avoid using it unless really necessary. It seems with each OS revision Apple makes it harder and harder to get AppleScripts to work outside of the user context, as you stated. Seems like we have to jump through higher and higher hoops each time to get them to work properly.

acdesigntech
Contributor II

@mm2270][/url -- I've finally got a chance at playing around with this, and I'm running into an issue dynamically populating the checkbox from an array... I'm experimenting with reading each item in an array and pushing that into a single variable for cocoaDialog, but as you might expect that just gives me one long item to check, rather than each individual item

PopulatedApps=`for (( a=0; a < $SoftwareArraySize; a=a+1 )); do
    echo "${SoftwareArray[$a]} c"
done`

If I populate the --items parameter directly with a for loop, i get a jumbled mess of nonsense if i first change IFS to be $

--items $(for (( a=0; a < $SoftwareArraySize; a=a+1 )); do
    echo "${SoftwareArray[$a]}"
done) 

and finally I get the nicest looking output if I don't change IFS, but then each word is it's own check box, and that's not right either. I know I'm missing something simple here. Just can't think of it right now. Any ideas?

I mean, I could always just cop out and remove the spaces in software titles, but this should work without having to do that

mm2270
Legendary Contributor III

Hey @acdesigntech][/url, I guess I'm not completely following. Is your ${SoftwareArray[@]} already populated manually in your script somewhere with values, or is it being dynamically generated somehow? I'm assuming the latter, but I don't actually see where you're getting the items for the array from in your code above.

If the list is actually hardcoded somewhere, as long as you quote the array in the cocoaDialog line, like:
--items "${SoftwareArray[@]}" it should work.

As for generating an array dynamically, I've been doing this a lot in some of my scripts lately, and I use something along these lines. For example, if I wanted to generate an application list from the Applications folder:

#!/bin/bash

while read app; do
    AppList+=("${app}")
done < <(ls /Applications)

/path/to/cocoaDialog.app/Contents/MacOS/cocoaDialog checkbox 
--items "${AppList[@]}" --label "Choose applications:" --columns 2 --button1 "Choose"

The above handles spaces in the items correctly, so you would get checkboxes like "Quicktime Player" on one line, not "Quicktime" and "Player" on separate lines.
Note that this is using process substitution, because the array contents won't get held in a normal loop process. IOW, with something like:

ls /Applications | while read app; do

once you exit the while loop, the array is discarded since it only exists within the sub-shell. Process substitution gets around that, but it only works with bash (and some other shells), not with the Bourne shell, so #!/bin/bash, not #!/bin/sh in your script.

Does that help at all?

acdesigntech
Contributor II

I knew it was something simple!!! Thanks!

I'm actually dynamically populating a single array and duct taping it to pretend it's a 2-dimensional array since bash does not handle those. EDIT: I guess it's a 3-dimensional array since there are 3 elements to each app. I suppose a higher level language might be better to code in for what i'm trying to do... oh well

Like so: ```

!/bin/sh

fPopulateAppsArrays ()
{ echo "We are now in the fPopulateAppsArrays function" DepartmentalApps=() DAIndex=0

while read line do AppName=echo "$line" | cut -d , -f1 InstallerName=echo "$line" | cut -d ',' -f2 UpdaterName=echo "$line" | cut -d ',' -f3 DepartmentalApps[$DAIndex]="$AppName" DAIndex=$(( $DAIndex 1 )) DepartmentalApps[$DAIndex]="$InstallerName" DAIndex=$(( $DAIndex 1 )) if [ "$UpdaterName" != "" ]; then DepartmentalApps[$DAIndex]="$UpdaterName" else DepartmentalApps[$DAIndex]="NA" fi done < /Volumes/"$CasperDP"/Mac-UserMigration/@DepartmentalApplications/DepartmentalAppsList.txt

the comma separated file lets me specify app names, their associated installer packages, any update packages that get applied, and so forth \- and the tech only needs to see the app name.

I can then loop through an array like this to get the exact position of the multiple indices in a the single array:

$a is the App name, $i is the install type: Manual denotes a manual install that needs to happen after-the-fact, X denotes software that is not supported at AG, No108 denotes software that is no longer supported on OS 10.8+, or $i is the name of the installer package, $u is the updater package, if any. Increase the step by 3, not 1 for each iteration of the loops to account for : App name, App installer, and Updater if any. (If no updater, then NA)

for (( a=0, i=1, u=2; a < $DeptAppsSize; a=a+3, i=i+3, u=u+3 )); do...
```

and for some reason i always forget about the while..do subshell, and then spend 15 minutes wondering why my array/variable/textfile ceases to exist. Then go "duuuuuuhhhhh" and fix it.

scottb
Honored Contributor

@mm2270 - did anything ever come of this? I imagine now it would not include Cocoa D, but wondering if you ever crafted anything along these lines?

thanks!

mschroder
Valued Contributor

Isn't it much easier and cleaner to associate different package groups to 'sites', and let the users decide which site to join when enrolling?

scottb
Honored Contributor

@mschroder - 100%
It's a client request a teammate got. I don't see why, but I'm not the client ;)

m_donovan
Contributor III

I am using something similar as a way to learn python. I have a GUI with check boxes that are associated to Static group names. The checkbox(es) selected create a list of group IDs that then get used to add the serial number to the static group via a PUT api call. If you are interested I can post the script later but as I said I am using this to learn python so it probably needs a lot of work.

scottb
Honored Contributor

@m.donovan

I would be interested in order to offer something up for my colleague to get a look at. Works in progress are great to post up as quite often, those more smarterer offer up help on cleaning it up!

m_donovan
Contributor III

Here it is. I encrypt the api username and password with a salted hash that is passed in via the Jamf script parameters. You can hard code them if you want. The static groups are already scoped to the apps and this is just adding or removing computers to/from the static groups. Both the encryption script and the base for this app I got from Bryson Tyrrell's GitHub. I apologize for the lack of comments. I was just using this as a way to better understand Python and was planning on commenting it later. If you have any questions please let me know. I hope this helps.

#!/usr/bin/python

import AppKit
import urllib2
import base64
import platform
import subprocess
import sys

if sys.version_info < (3, 0):
    # Python 2
    import Tkinter as tk
else:
    # Python 3
    import tkinter as tk

auH = sys.argv[4]
apH = sys.argv[5]

def DecryptString(inputString, salt, passphrase):
    '''Usage: >>> DecryptString("Encrypted String", "Salt", "Passphrase")'''
    p = subprocess.Popen(['/usr/bin/openssl', 'enc', '-aes256', '-d', '-a', '-A', '-S', salt, '-k', passphrase], stdin = subprocess.PIPE, stdout = subprocess.PIPE)
    return p.communicate(inputString)[0]

api_user = DecryptString(auH, "a0b740a443234587fa1275", "0453455efd854df5e73517bfc2e79").strip()
api_password = DecryptString(apH, "583b483f4084543e2191", "18e8f98f4be245454b0bbe4737a18").strip()
jss_url = "https://yourjamfproserver:8443"


def update_static_group(ser_number, grp_id, update_type):

    xml = "<computer_group><" + update_type + "><computer><serial_number>" + ser_number + "</serial_number></computer></" + update_type + "></computer_group>"

    request = urllib2.Request(jss_url + "/JSSResource/computergroups/id/" + grp_id)
    request.add_header('Authorization', 'Basic ' + base64.b64encode(api_user + ':' + api_password))
    request.add_header('Content-Type', 'text/xml')
    request.get_method = lambda: 'PUT'
    response = urllib2.urlopen(request, xml)
    print(response)

class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.pack()
        self.master.title("Scope Manager")
        self.master.resizable(False, False)
        self.master.tk_setPalette(background='#ececec')

        self.master.protocol('WM_DELETE_WINDOW', self.click_cancel)
        self.master.bind('<Return>', self.click_reset)
        self.master.bind('<Escape>', self.click_cancel)

        x = (self.master.winfo_screenwidth() - self.master.winfo_reqwidth()) / 2
        y = (self.master.winfo_screenheight() - self.master.winfo_reqheight()) / 3
        self.master.geometry("+{}+{}".format(x, y))

        self.master.config(menu=tk.Menu(self.master))

        self.grp_list = []

        self.status_msg = tk.StringVar()

        self.var1 = tk.IntVar()
        self.var2 = tk.IntVar()
        self.var3 = tk.IntVar()
        self.var4 = tk.IntVar()
        self.var5 = tk.IntVar()
        self.var6 = tk.IntVar()
        self.var7 = tk.IntVar()
        self.var8 = tk.IntVar()
        self.var9 = tk.IntVar()
        self.var10 = tk.IntVar()
        self.var11 = tk.IntVar()
        self.var12 = tk.IntVar()
        self.var13 = tk.IntVar()
        self.var14 = tk.IntVar()
        self.var15 = tk.IntVar()
        self.var16 = tk.IntVar()
        self.var17 = tk.IntVar()

        self.var1.set(0)
        self.var2.set(0)
        self.var3.set(0)
        self.var4.set(0)
        self.var5.set(0)
        self.var6.set(0)
        self.var7.set(0)
        self.var8.set(0)
        self.var9.set(0)
        self.var10.set(0)
        self.var11.set(0)
        self.var12.set(0)
        self.var13.set(0)
        self.var14.set(0)
        self.var15.set(0)
        self.var16.set(0)
        self.var17.set(0)

        dialog_frame = tk.Frame(self)
        dialog_frame.pack(padx=20, pady=15)

        tk.Label(dialog_frame, text="1. Input a serial number or serial number list using ','.
2. Select an application or applications to scope.
3. Click button").pack()

        input_frame = tk.Frame(self)
        input_frame.pack(padx=20, pady=15)

        tk.Label(input_frame, text='Serial #(s):').grid(row=0, column=0, sticky='w')

        self.serial_input = tk.Entry(input_frame, background='white', width=24)
        self.serial_input.grid(row=0, column=1, sticky='w')
        self.serial_input.focus_set()

        chk_btn_frame = tk.Frame(self)
        chk_btn_frame.pack(anchor='w', padx=20, pady=15)

        chk1 = tk.Checkbutton(chk_btn_frame, text='iMovie', variable=self.var1)
        chk2 = tk.Checkbutton(chk_btn_frame, text='iMovie SS', variable=self.var2)
        chk3 = tk.Checkbutton(chk_btn_frame, text='Garageband', variable=self.var3)
        chk4 = tk.Checkbutton(chk_btn_frame, text='Garageband SS', variable=self.var4)
        chk5 = tk.Checkbutton(chk_btn_frame, text='iWork', variable=self.var5)
        chk6 = tk.Checkbutton(chk_btn_frame, text='iWork SS', variable=self.var6)
        chk7 = tk.Checkbutton(chk_btn_frame, text='Apple Configurator', variable=self.var7)
        chk8 = tk.Checkbutton(chk_btn_frame, text='Apple Configurator SS', variable=self.var8)

        chk9 = tk.Checkbutton(chk_btn_frame, text='Photoshop SS', variable=self.var9)
        chk10 = tk.Checkbutton(chk_btn_frame, text='Acrobat Pro', variable=self.var10)
        chk11 = tk.Checkbutton(chk_btn_frame, text='Acrobat Pro SS', variable=self.var11)
        chk12 = tk.Checkbutton(chk_btn_frame, text='Animate SS', variable=self.var12)
        chk13 = tk.Checkbutton(chk_btn_frame, text='Audition SS', variable=self.var13)
        chk14 = tk.Checkbutton(chk_btn_frame, text='Illustrator SS', variable=self.var14)
        chk15 = tk.Checkbutton(chk_btn_frame, text='InDesign SS', variable=self.var15)
        chk16 = tk.Checkbutton(chk_btn_frame, text='Lightroom SS', variable=self.var16)
        chk17 = tk.Checkbutton(chk_btn_frame, text='Premier Pro SS', variable=self.var17)

        chk1.grid(row=0, column=0, sticky='w')
        chk2.grid(row=1, column=0, sticky='w')
        chk3.grid(row=2, column=0, sticky='w')
        chk4.grid(row=3, column=0, sticky='w')
        chk5.grid(row=4, column=0, sticky='w')
        chk6.grid(row=5, column=0, sticky='w')
        chk7.grid(row=6, column=0, sticky='w')
        chk8.grid(row=7, column=0, sticky='w')
        chk9.grid(row=0, column=1, sticky='w')
        chk10.grid(row=1, column=1, sticky='w')
        chk11.grid(row=2, column=1, sticky='w')
        chk12.grid(row=3, column=1, sticky='w')
        chk13.grid(row=4, column=1, sticky='w')
        chk14.grid(row=5, column=1, sticky='w')
        chk15.grid(row=6, column=1, sticky='w')
        chk16.grid(row=7, column=1, sticky='w')
        chk17.grid(row=8, column=1, sticky='w')

        chk1.configure(command=lambda chkd=chk1: self.set_list(chkd))
        chk2.configure(command=lambda chkd=chk2: self.set_list(chkd))
        chk3.configure(command=lambda chkd=chk3: self.set_list(chkd))
        chk4.configure(command=lambda chkd=chk4: self.set_list(chkd))
        chk5.configure(command=lambda chkd=chk5: self.set_list(chkd))
        chk6.configure(command=lambda chkd=chk6: self.set_list(chkd))
        chk7.configure(command=lambda chkd=chk7: self.set_list(chkd))
        chk8.configure(command=lambda chkd=chk8: self.set_list(chkd))
        chk9.configure(command=lambda chkd=chk9: self.set_list(chkd))
        chk10.configure(command=lambda chkd=chk10: self.set_list(chkd))
        chk11.configure(command=lambda chkd=chk11: self.set_list(chkd))
        chk12.configure(command=lambda chkd=chk12: self.set_list(chkd))
        chk13.configure(command=lambda chkd=chk13: self.set_list(chkd))
        chk14.configure(command=lambda chkd=chk14: self.set_list(chkd))
        chk15.configure(command=lambda chkd=chk15: self.set_list(chkd))
        chk16.configure(command=lambda chkd=chk16: self.set_list(chkd))
        chk17.configure(command=lambda chkd=chk17: self.set_list(chkd))

        button_frame = tk.Frame(self)
        button_frame.pack(padx=5, pady=(0, 5), anchor='e')

        #self.clicked=[]
        button1 = tk.Button(button_frame, text="Add", width=8)
        button2 = tk.Button(button_frame, text="Remove", width=8)
        button3 = tk.Button(button_frame, text="Reset", width=8)
        button4 = tk.Button(button_frame, text="Cancel", width=8)

        button1.pack(side='right')
        button2.pack(side='right')
        button3.pack(side='right')
        button4.pack(side='right')

        button1.configure(command=lambda btn=button1: self.add_to_grp(btn))
        button2.configure(command=lambda btn=button2: self.remove_from_grp(btn))
        button3.configure(command=lambda btn=button3: self.click_reset())
        button4.configure(default='active', command=lambda btn=button4: self.click_cancel())

    def click_reset(self, event=None):
        self.var1.set(0)
        self.var2.set(0)
        self.var3.set(0)
        self.var4.set(0)
        self.var5.set(0)
        self.var6.set(0)
        self.var7.set(0)
        self.var8.set(0)
        self.var9.set(0)
        self.var10.set(0)
        self.var11.set(0)
        self.var12.set(0)
        self.var13.set(0)
        self.var14.set(0)
        self.var15.set(0)
        self.var16.set(0)
        self.var17.set(0)

        self.grp_list = []
        self.serial_input.delete(0, 'end')

    def set_list(self, chkd):
        chk_text = chkd.cget("text")
        #print("The user selected " + chk_text)
        if chk_text == "iMovie":
            grp_id = "3270"
            ck_value = self.var1.get()
        elif chk_text == "iMovie SS":
            grp_id = "3418"
            ck_value = self.var2.get()
        elif chk_text == "Garageband":
            grp_id = "3282"
            ck_value = self.var3.get()
        elif chk_text == "Garageband SS":
            #grp_id = "3440" #for testig
            grp_id = "3416"
            ck_value = self.var4.get()
        elif chk_text == "iWork":
            grp_id = "3407"
            ck_value = self.var5.get()
        elif chk_text == "iWork SS":
            #grp_id = "3441" #for testing
            grp_id = "3417"
            ck_value = self.var6.get()
        elif chk_text == "Apple Configurator":
            grp_id = "3285"
            ck_value = self.var7.get()
        elif chk_text == "Apple Configurator SS":
            grp_id = "3442"
            ck_value = self.var8.get()
        elif chk_text == "Photoshop SS":
            grp_id = "3203"
            ck_value = self.var9.get()
        elif chk_text == "Acrobat Pro":
            grp_id = "3196"
            ck_value = self.var10.get()
        elif chk_text == "After Effects":
            grp_id = "3197"
            ck_value = self.var11.get()
        elif chk_text == "Animate SS":
            grp_id = "3198"
            ck_value = self.var12.get()
        elif chk_text == "Audition SS":
            grp_id = "3199"
            ck_value = self.var13.get()
        elif chk_text == "Illustrator SS":
            grp_id = "3200"
            ck_value = self.var14.get()
        elif chk_text == "InDesign SS":
            grp_id = "3201"
            ck_value = self.var15.get()
        elif chk_text == "Lightroom SS":
            grp_id = "3202"
            ck_value = self.var16.get()
        elif chk_text == "Premier Pro SS":
            #grp_id = "3355" #for testing
            grp_id = "3204"
            ck_value = self.var17.get()


        if ck_value == 1:
            self.grp_list.append(grp_id)
        elif ck_value == 0:
            self.grp_list.remove(grp_id)

        #print(self.grp_list)


    def click_cancel(self, event=None):
        print("The user clicked 'Cancel'")
        self.master.destroy()

    def add_to_grp(self, btn):
        upd_typ = "computer_additions"
        text = btn.cget("text")
        print("The user clicked " + text)
        ser_list = self.serial_input.get()
        print(ser_list)
        serials = ser_list.split(",")
        for ea_ser in serials:
            for ea_grp in self.grp_list:

                print("Serial: " + ea_ser + " Group: " + ea_grp )
                update_static_group(ea_ser, ea_grp, upd_typ)


    def remove_from_grp(self, btn):
        upd_typ = "computer_deletions"
        text = btn.cget("text")
        print("The user clicked " + text)
        ser_list = self.serial_input.get()
        print(ser_list)
        serials = ser_list.split(",")
        for ea_ser in serials:
            for ea_grp in self.grp_list:
                print("Serial: " + ea_ser + " Group: " + ea_grp )
                update_static_group(ea_ser, ea_grp, upd_typ)





if __name__ == '__main__':
    info = AppKit.NSBundle.mainBundle().infoDictionary()
    info['LSUIElement'] = True

    root = tk.Tk()
    app = App(root)
    AppKit.NSApplication.sharedApplication().activateIgnoringOtherApps_(True)
    app.mainloop()

"""
There are a few items left to address before we have a working GUI that matches the standard macOS dialogs. When running
a Python app using Tkinter the Python launcher's icon will appear in the Dock. This can be dynamically flagged to run
without the Dock icon by loading and manupulating the launcher's 'Info.plist' before the main window has been created.
'AppKit' will be used in our main function to do this.

Lastly, to allow the user to control the window as they would any other macOS dialog, we will bind the '<Return>' and
'<Escape>' keys to our 'OK' and 'Cancel' methods. We will also override the function of the close button on the GUI
window to point to our 'Cancel' method to keep control of the teardown.

Because we are now using alternative triggers for these funtions we must add an 'event' parameter to accept the callback
that is passed by the binding. The default value is set to 'None' for when the buttons are used.
"""