diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9d18c6 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# SCL Jamf Tools + +This repository contains a collection of tools written to perform as enhancements to the Jamf Pro management software. You can read about them at our [blog post](https://apple.lib.utah.edu/?p=2057) or in the indiviual project areas. + +Our goal is to make genericized versions of our internal tools available to other Jamf administrators. They are usable as is, but are intended as frameworks that others can use to build custom tools to more closely fit their environments. + +## Contents + +- [Contact](#contact) +- [Cargo Ship](#cargoship) +- [Tugboat](#tugboat) +- [Update History](#update-history) + +## Contact + +If you have any comments, questions, or other input, either [file an issue](../../issues) or [send us an email](mailto:mlib-its-mac-github@lists.utah.edu). Thanks! + + + +## [Cargo Ship](cargo_ship/README.md) + +![](imgs/cargoship_final.png) + +Cargo Ship is designed as a reflection tool, it shows you what should be on a specific client. It can read from Jamf Pro directly from ID's or by selecting from search results. + + + +## [Tugboat](tugboat/README.md) + +![](imgs/tugboat_final.png) + +Tugboat is designed to make modifications to specific computer records on your Jamf Pro server. + + + +## Update History + +| Date | Version | Notes | +| --------- | ------- | ---------------- | +| 2017.2.15 | | Initial release. | \ No newline at end of file diff --git a/cargo_ship/Cargo Ship application.dmg b/cargo_ship/Cargo Ship application.dmg new file mode 100644 index 0000000..1add31e Binary files /dev/null and b/cargo_ship/Cargo Ship application.dmg differ diff --git a/cargo_ship/README.md b/cargo_ship/README.md new file mode 100644 index 0000000..c520279 --- /dev/null +++ b/cargo_ship/README.md @@ -0,0 +1,147 @@ +# Cargo Ship + +![](../imgs/cargoship_final.png) + +Cargo Ship is designed as a kind of reflection tool, it shows you what Jamf thinks should be on a specific client. It can read from Jamf Pro directly from ID's or by selecting from search results. + +This tool is identical to our inhouse version. However it can be customized to fit your environment by modifying it's Python code. + +*Note: Offering a tool designed from the outset to be customized at the source level is a new venture for us. If you feel the documentation we've included is not sufficient to help you successfully navigate the source code, please consider raising an issue and we'll do our best to correct any short comings.* + +## Contents + +- [Download](#download) - get the .dmg +- [Contact](#contact) +- [System Requirements](#system-requirements) +- [Install](#install) +- [Uninstall](#uninstall) +- [Purpose](#purpose) +- [Usage](#usage) + - [How Does It Work](#how-does-it-work) + - [Jamf User Privileges](#jamf-user-privileges) +- [Notes](#notes) +- [Update History](#update-history) + + + +## Download + +[Download the latest installer here!](../../releases/) + + + +## Contact + +If you have any comments, questions, or other input, either [file an issue](../../issues) or [send us an email](mailto:mlib-its-mac-github@lists.utah.edu). Thanks! + + + +## System Requirements + +- Python 2.7+ (which you can download [here](https://www.python.org/download/)) + +If you intend to rebuild customized versions you will need the following tools, depending on your platform: + +- py2app for MacOS applications (which you can download [here](https://pythonhosted.org/py2app/install.html)) +- cx_freeze for Windows applications (which you can download [here](https://anthony-tuininga.github.io/cx_Freeze/)) + + + +## Install + +Place the application in an appropriate location for your environment. + + + +## Uninstall + +Remove the application. + + + +## Purpose + +The idea behind Cargo Ship began as an offhand remark about how useful it would be to have a tool that could tell you what a machine was supposed to be, what policies applied to it, what groups it belonged to, etc. It also served as a learning tool for the author. + + + +## Usage + +On opening the application, provide your Jamf Pro server address, and a user with appropriate privileges. + +![](/Volumes/Data/Users/u0942941/Desktop/login.png) + +The first line in the UI contains all of the navigation tools. + +![](imgs/cargoship_usage.png) + +**This Device** Open the record for the device the application is running on. + +**Other ID** If you know the Jamf ID of the specific machine you'd like to see, enter it in the text field and press the Other ID button. + +**Search Jamf** Enter the information you'd like to search for in the text field and press the Search Jamf button. The following image shows the search results window. + +![](imgs/search_results.png) + +Select the machine you'd like to see and its record will load into the display. + + + +#### How Does It Work + +Here are the steps that are performed when the application is launched: + +1. The user provides the Jamf Pro server address and the user name and password for an account with access to a specific areas of the Jamf database. This area can be customized to include your Jamf server address. +2. The application downloads a list of all policies in the database. This list contains the ID and internal "name" of the policy, which isn't really the proper name. With the list of ID's, it asks the Jamf server for specific information about each identified policy. A list is constructed containing the ID, actual policy name and other qualifying information. This list is then added to a cumulative list of policies. +3. It then downloads the list of profiles (osxconfigurationprofiles). With this list, a cumulative dictionary is built using profile ID as the key, and the name of the profile as the value. +4. *The time required to complete the two previous steps is dependent on the number of policies and profiles defined in your environment. It may take minutes to complete.* +5. The empty UI is created. +6. The user specifies which machine to investigate. +7. The full computer record for the specified machine is downloaded. +8. Specific items are pulled directly from the record and displayed: + - Computer Name + - Jamf ID + - Computer users full name + - Last checkin time to the Jamf server +9. Specific items are parsed from the record and displayed: + - Printers + - Computer groups + - Extension attributes + - Installed software (by a local installer package and by Jamf itself) +10. Policies and profiles are cross-referenced between the full record and precreated data structures and finally displayed. + + + +#### Jamf User Privileges + +In order to use Jamf's API, your users will need the appropriate rights to certain areas of the database. To check these settings go to the following area: **All Settings**, **System Settings**, **JSS User Accounts & Groups**. + +![](imgs/jss_privs.png) + + + +This chart shows the required privileges for Cargo Ship to operate properly: + +| Field | Create | Read | Update | Delete | Notes | +| ----------------------------- | :----: | :--: | :----: | :----: | :----------------------------- | +| Accounts and Groups | | ☑ | | | Needed for login functionality | +| Computer Extension Attributes | | ☑ | | | | +| Computers | | ☑ | | | | +| OS X Configuration Profiles | | ☑ | | | | +| Policies | | ☑ | | | | + +These requirements are also included in the login method. If you make customizations, you may need to add these additional areas to the list of required privileges. + +## Notes + +Fomerly known as "**The Summerizerator**". + +My heartfelt thanks to the other members of the Mac Group and the IT administration of the Marriott Library for their support, bug reports and feature requests! + + + +## Update History + +| Date | Version | Notes | +| --------- | ------- | ----------------------- | +| 2017.2.15 | 1.0.0 | Initial public release. | \ No newline at end of file diff --git a/cargo_ship/cargo_ship.icns b/cargo_ship/cargo_ship.icns new file mode 100644 index 0000000..c354af1 Binary files /dev/null and b/cargo_ship/cargo_ship.icns differ diff --git a/cargo_ship/cargo_ship.ico b/cargo_ship/cargo_ship.ico new file mode 100644 index 0000000..4b8b544 Binary files /dev/null and b/cargo_ship/cargo_ship.ico differ diff --git a/cargo_ship/cargo_ship.py b/cargo_ship/cargo_ship.py new file mode 100755 index 0000000..efb7a82 --- /dev/null +++ b/cargo_ship/cargo_ship.py @@ -0,0 +1,1144 @@ +#!/usr/bin/python + +# Copyright (c) 2017 University of Utah Student Computing Labs. ################ +# All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appears in all copies and +# that both that copyright notice and this permission notice appear +# in supporting documentation, and that the name of The University +# of Utah not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. This software is supplied as is without expressed or +# implied warranties of any kind. +################################################################################ + +# cargo_ship.py ################################################# +# +# A Python Tk application to view Jamf computer records. +# +# +# 1.0.0 2017.02.15 Initial public release. tjm +# +# +################################################################################ + +# notes: ####################################################################### +# +# py2app: +# rm -rdf build dist ; /usr/bin/python setup.py py2app -s +# +# +# +# +################################################################################ + +from __future__ import print_function +from Tkinter import * +import ttk +import tkMessageBox +import subprocess +import os +import re +import urllib +import urllib2 +import json +import base64 +import locale +import sys +import platform +import webbrowser +import ScrolledText +from xml.dom.minidom import parseString + +class Summarize(object): + """ + Store keys, manipulate keys, output script and build the package + """ + + def __init__(self, root, jamf_hostname, jamf_username, jamf_password): + """ + Initialize object and variables + """ + self.root = root + self.jamf_hostname = jamf_hostname + self.jamf_username = jamf_username + self.jamf_password = jamf_password + self.local_jamf_id = None + + self.computer_name_string = StringVar() + self.fullname_string = StringVar() + self.search_string = StringVar() + self.status_string = StringVar() + self.checkin_string = StringVar() + self.id_string = StringVar() + + self.status_string.set("Ready.") + + self.status_warning = ttk.Style() + self.status_warning.configure('Warning.TLabel', foreground='red') + + self.status_normal = ttk.Style() + self.status_normal.configure('Normal.TLabel', foreground='black') + + # + # These methods are time intensive based on the number of each in your database + self.jamf_policies = self.build_policies() + self.jamf_profiles = self.build_profiles() + + self.build_ui() + + def build_ui(self): + """ + Build UI + """ + + # + # This is an encoded gif of the title image + self.logo_image = '''\ + R0lGODlhWAJRAPcAAAEAAAwMDQMJCwwMEwUNExEOFQwSFAsVGwgVGhITExUVHBcZGhIPDScYGCch + HA0aIgkYJRMdJBgcJQ8aLg0gKhUiKxwmLR0oLxciJxwmMh4pMRopOBouNSYmJiUoKjQqJyIrMyQr + Nys0PCY0OTc1OjUyLyQfHlQREEc6OVUvMG0yNWYjJRQ1TSk3RDU8QiE+VFA9R3I5QjhGSjtTWDJI + UxlMajFTay1TZ0JCQkdJS1lHSERMU0hVWlhYWFZVWFNOU2dHSnVISGhLU2lXWndYWXNPUGxkXEpa + ZVdcZEtTbmlbYndbYmtacUtjaVlmakpld1hqdV1xd2RrbGxra2dlZXxsbHZmZ2tybndza2ZtdHhq + c21zdGx1e2d2eXN0dHR7fHx8fXh4dltkV1VCOY4vNsQSMMcfO8chPMgjPpZMNJI8SKc7S846U8kq + RIhaWJhWWpBJUahMT5NjW6lpV5daYYJcZ6ZYZIVoaZdoaId4eJR3eI9vcKVmZ61wdNFGXs9BWdJM + Y9RSZ9RVatlnetdsRHqCfWqCfoSEfZSDeqWFejVokydumlFzjGh5hXR7g3p9gnN3h254kFJ8oYV9 + g4t3hNtugN13iHyDhHiFiWqGl3qIlXSJjnmUpWyRrFmMqE+Xw4ODg4KFioWLi4yNjImIhpSIioqS + jZWUjYOMk4uNkoeKl4yTlIyUm4WVmJOUlJOWm5SanJycnJmXlpSMlKOZmqWRhpyhnaWjnbKnmoqa + pZScopucooqWqZ2ipJikqZmotoyovKOkpKKlq6WqrKqqqqmopbSsqa2xrLSzraess6yytKu1ubK0 + tLS6vLi5ube2uLKus6mfpOKKmeGFlOSQnuaap+qrtsK7ur7Cvc/DvZSzyaa5x7a8wae91NO9w7zD + xrjI1KzF2LfR5cXFxcfKysbN1MfT2tHR0d3d3djY2tHOzPDCyu/O0snZ5tXb5djj6tjo9crg8+np + 6ePn6+Tt9ez0+fT09Pzz9f////X3+vvv8eLd5QAAAAAAAAAAAAAAACH5BAAAAAAALAAAAABYAlEA + AAj+APEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuX + MGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0aMZ67eLVy2e0qdOnUKNKnXowXzhg9PDVi0e1q9evYJ+2 + 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 + btHp2bOw573pYxdGEDfRRzUELqyh1OPCLHD2tzFFgE4pQAERcEBo1h0CElS84nmzuAx+YPGKw8AF + KLD4ByISmggsIAGFWtEBOlCCEsgABzCGcSb2PfMgKYkYRJ4ZVAY7Y33TvLCc7ndg/n2TaQxiEJWo + Bl7yoQ1xIwACBoD6AAoQAKozgAEL+JADHPABFKAABilAARB0IHYgAEEIZh+7DtTe9RJ8wAENMEED + GpAAky9gASZAbQc+wHK+l8ADzPB54HciW7og0sE9F/xOgX7sodvEGtVYByDWMQ1rKP0FBpjA0wtQ + AKgXoFObn3oBFJAAqwf+IAGnD8DVG7AABihA9ApY/egZEIABVN3upD+9yU3w9g/03vcrx6U99Jz4 + xJ+Dvlyh0+GTSfyfMz4n1lgHO9hgjcfn5Rcac9QBXO/5z9ee9lOnfdU3H4CCCSAACpCABBRAdfZT + vQCnx33q4W/yBmh97yBHAQ5KUAjm9z93QoYKKYC05UK8/lMoTjsDM1DABVTABGTAM4BABmzABXRA + wGi8m1gHS1iHx7MEdtCLG4CAUxk9BeiUTjGA2ivB1Gu/FRQA0UOt9VtBqkM9kzs9q4O/+tu6Eug6 + FBiDEhhAA9y3dCCGWJCCKpCCUiCGLNs1KWDCIsw0IPyqe5DCKaTCKrSpwiu0Qp6ohkpgh0qovLzI + h26ogANQv6gzFEMxQQFgONoTABSkPRRkAAnoABLsFPJ7wxQ8Qc97P4cjvQXYuvvDART4AUODwkI0 + xENExL54ggzwAAxIPzIUEYd7AIfTPoezxEt0OAlwgRzwAEz0xEzMxAVIPz/0w/tDAR0Yg2JIxFVk + xVZ0xaEYvleUxVmkxVq0xVvExVzUxV3kxV70xV8ExmAUxmEkxpwICAAh/wtYTVAgRGF0YVhNUDw/ + eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4Onht + cG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUu + Ni1jMTM4IDc5LjE1OTgyNCwgMjAxNi8wOS8xNC0wMTowOTowMSAgICAgICAgIj4KIDxyZGY6UkRG + IHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+ + CiAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIvPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0 + YT4KPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e + 3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6al + pKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1s + a2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQz + MjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAADs= + ''' + + self.root.title("Cargo Ship") + + self.mainframe = ttk.Frame(self.root) + self.mainframe.grid(column=0, row=0, sticky=NSEW) + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + self.root.geometry('+0+0') + + # + # values for column widths + single_width_column = 40 + double_width_column = 84 + + # + # position and display logo image + self.logo_label = ttk.Label(self.mainframe) + self.logo_photoimage = PhotoImage(data=self.logo_image) + self.logo_label['image'] = self.logo_photoimage + self.logo_label.grid(column=1, row=0, columnspan=10) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=30, columnspan=35, sticky=EW) + + # + # these are the UI navigation elements, if there is a specific button or + # extension attribute you want to feature it should be added in this section. + ttk.Label(self.mainframe, text="Summarize:").grid(column=1, row=40, sticky=W) + ttk.Button(self.mainframe, text="This Device", command=self.query_jamf_me).grid(column=1, row=40) + ttk.Button(self.mainframe, text="Other ID", width=6, command=self.query_jamf_id).grid(column=1, row=40, sticky=E) + + self.host_entry = ttk.Entry(self.mainframe, width=10, textvariable=self.id_string) + self.host_entry.config(font=('', 12, 'bold')) + self.host_entry.grid(column=2, row=40, sticky=EW) + + ttk.Button(self.mainframe, text="Search Jamf", command=self.search_string_jamf).grid(column=3, row=40, sticky=E) + self.search_entry = ttk.Entry(self.mainframe, width=20, textvariable=self.search_string) + self.search_entry.config(font=('', 12, 'bold')) + self.search_entry.grid(column=4, row=40, sticky=EW) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=55, columnspan=35, sticky=EW) + + ttk.Label(self.mainframe, text="Computer Name:").grid(column=1, row=60, sticky=E) + self.cname_display = ttk.Entry(self.mainframe, width=31, state="readonly", textvariable=self.computer_name_string) + self.cname_display.config(font=('', 12, 'bold')) + self.cname_display.grid(column=2, row=60, sticky=EW) + + ttk.Label(self.mainframe, text="Last Checkin:").grid(column=3, row=60, sticky=E) + self.checkin_display = ttk.Entry(self.mainframe, width=31, state="readonly", textvariable=self.checkin_string) + self.checkin_display.config(font=('', 12, 'bold')) + self.checkin_display.grid(column=4, row=60, sticky=EW) + + ttk.Label(self.mainframe, text="User's Name:").grid(column=1, row=70, sticky=E) + self.uname_display = ttk.Entry(self.mainframe, width=31, state="readonly", textvariable=self.fullname_string) + self.uname_display.config(font=('', 12, 'bold')) + self.uname_display.grid(column=2, row=70, sticky=EW) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=150, columnspan=35, sticky=EW) + + # + # theses are the large output panes + ttk.Label(self.mainframe, text="Printers:", font="TkHeadingFont").grid(column=1, row=160, sticky=EW) + self.printer_field = ScrolledText.ScrolledText(self.mainframe, width=single_width_column, height=15, wrap='none') + self.printer_field.grid(column=1, row=165, pady=1, padx=1) + + ttk.Label(self.mainframe, text="Computer Groups:", font="TkHeadingFont").grid(column=2, row=160, sticky=EW) + self.group_field = ScrolledText.ScrolledText(self.mainframe, width=single_width_column, height=15, wrap='none') + self.group_field.grid(column=2, row=165, pady=1, padx=1) + + ttk.Label(self.mainframe, text="Profiles:", font="TkHeadingFont").grid(column=3, row=160, sticky=EW) + self.jamf_profiles_field = ScrolledText.ScrolledText(self.mainframe, width=single_width_column, height=15, wrap='none') + self.jamf_profiles_field.grid(column=3, row=165, pady=1, padx=1) + + ttk.Label(self.mainframe, text="Policies:", font="TkHeadingFont").grid(column=4, row=160, sticky=EW) + self.jamf_policies_field = ScrolledText.ScrolledText(self.mainframe, width=single_width_column, height=15, wrap='none') + self.jamf_policies_field.grid(column=4, row=165, pady=1, padx=1) + + ttk.Label(self.mainframe, text="Extension Attributes:", font="TkHeadingFont").grid(column=1, row=180, sticky=W) + self.ea_field = ScrolledText.ScrolledText(self.mainframe, width=double_width_column, height=15, wrap='none') + self.ea_field.grid(column=1, row=185, columnspan=2, pady=1, padx=1) + + ttk.Label(self.mainframe, text="Packages:", font="TkHeadingFont").grid(column=3, row=180, sticky=W) + self.package_field = ScrolledText.ScrolledText(self.mainframe, width=double_width_column, height=15, wrap='none') + self.package_field.grid(column=3, row=185, columnspan=2, pady=1, padx=1) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=290, columnspan=35, sticky=EW) + + # + # status bar and quit button + self.status_label = ttk.Label(self.mainframe, textvariable=self.status_string) + self.status_label.grid(column=1, row=300, sticky=W, columnspan=50) + + ttk.Button(self.mainframe, text="Quit", command=self.root.destroy).grid(column=4, row=300, sticky=E) + + def search_string_jamf(self): + """ + This method handles searching Jamf with a string + """ + def double_click(*event): + """ + handle clicks + """ + + # + # when a click occurs, parse out ID from string and call query method + selected = listbox.get(listbox.curselection()) + trim_select = re.search(r'\((.*)\)', selected).group(1) + + self.id_string.set(trim_select) + self.query_jamf_id() + + self.root.lift() + + if self.search_string.get() == "" or self.search_string.get().replace(" ", "") == "": + if self.id_string.get(): + self.query_jamf_id() + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Searched for ID.") + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("No search string entered.") + + else: + # + # erase previous displayed values + self.reset_display() + + # + # encode special characters included in search string + url = self.search_string.get() + url = urllib.quote(url, ':/()') + url = self.jamf_hostname + '/JSSResource/computers/match/*' + url + '*' + url = urllib.quote(url, ':/()') + + # + # communicate with Jamf server + try: + request = urllib2.Request(url) + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Request error.")) + elif error.code == 401: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Authorization error.")) + elif error.code == 403: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Permissions error.")) + elif error.code == 404: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource not found.")) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0])) + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Generic error.")) + + return + except urllib2.URLError, error: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error contacting JAMF server.") + return + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying Jamf. [%s]" % exception_message) + return + + # + # begin parsing data returned from Jamf + jamf_dom = parseString(response.read()) + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("%i matches returned." % int(jamf_dom.getElementsByTagName('size')[0].childNodes[0].nodeValue)) + + match_results = [] + + # + # parse each returned computer element, retaining Jamf ID and Computer name + # properly format value to display + # build a version of the computer name used to sort by + # these rules are for our environment and may have no effect in yours. + # if the name is in the format "labmac-1" the integer is expanded to -0001 + # if the name is in the format "[lost] labmac-1" the sorting name is stored as + # "labmac-1a" to differentiate it from "labmac-1" + # the values are added to a list containing the previously processed values as + # [sorting name, computer name, jamf id] + + for node in jamf_dom.getElementsByTagName('computer'): + match_id = int(node.getElementsByTagName('id')[0].childNodes[0].nodeValue) + try: + match_name = node.getElementsByTagName('name')[0].childNodes[0].nodeValue + except: + match_name = "Not named." + + name_trim = match_name + + try: + number_part = re.search(r'(\d+)', name_trim).group(1) + number_free = "".join([i for i in name_trim if not i.isdigit()]) + expanded_number = '{:04d}'.format(int(number_part)) + expanded_x = number_free + expanded_number + name_trim = expanded_x + except: + pass + + if "[" in name_trim: + name_trim = re.search(r']([ -]*)(.*)', name_trim).group(2) + name_trim = str(name_trim) + "a" + + match_results.append([name_trim, match_name, match_id]) + + + # + # if there were returned results, build and display search results window + # + # + # position results window next to the main window, even if it has moved + # from the original location + # while sorting the list based on the synthetic string, + # display the computer name and ID + # bind clicks to function + if match_results: + + search_window = Toplevel() + + split_geom = self.root.winfo_geometry().split("+") + r_h = int(split_geom[0].split("x")[0]) + r_pos_x = int(split_geom[1]) + r_pos_y = int(split_geom[2]) + + search_window_geo = "%ix%i+%i+%i" % (190, 400, (r_h + r_pos_x + 10), (r_pos_y)) + search_window.geometry(search_window_geo) + + search_window.title("Search results") + + list_frame = ttk.Frame(search_window, width=190, height=400, padding=(4, 0, 0, 0)) + + scrollbar = Scrollbar(list_frame) + scrollbar.pack(side=RIGHT, fill=Y) + + listbox = Listbox(list_frame, bd=0, yscrollcommand=scrollbar.set, selectmode=SINGLE, width=190, height=400) + listbox.pack() + + scrollbar.config(command=listbox.yview) + + list_frame.pack() + + for item in sorted(match_results): + insert_string = item[1] +" (" + str(item[2]) + ")" + listbox.insert(END, insert_string) + + listbox.bind("<>", double_click) + + search_window.mainloop() + + def build_profiles(self): + """ + Fetch and build profile data structures + """ + # + # communicate with Jamf and grab generic profile list + # parse list, building dictionary along the way + # this should proceed quickly. + + # + # communicate with Jamf server + try: + url = self.jamf_hostname + '/JSSResource/osxconfigurationprofiles' + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Request error."))) + elif error.code == 401: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Authorization error."))) + elif error.code == 403: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Permissions error."))) + elif error.code == 404: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Resource not found."))) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0]))) + else: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Generic error."))) + + sys.exit() + except urllib2.URLError, error: + tkMessageBox.showerror("Error", "Error contacting JAMF server.") + sys.exit() + except Exception as exception_message: + tkMessageBox.showerror("Error", ("Error querying Jamf. [%s]" % exception_message)) + sys.exit() + + tmp_profiles = {} + + for item in response_json['os_x_configuration_profiles']: + tmp_profiles[item["id"]] = item["name"] + + return tmp_profiles + + def build_policies(self): + """ + fetch and build policy data structures + """ + # + # communicate with Jamf and grab generic policy list + # + # communicate with Jamf and grab each individual policy record + # --- this is the slow bit --- + # with each record + # retain name, id and if the policy applies to all computers + # retain IDs of specific computers the policy applies to + # retain ID's and names of computer groups the policy applies to + # add these values as a list to previously processed policies + # this will not proceed quickly. + + # + # communicate with Jamf server + try: + url = self.jamf_hostname + '/JSSResource/policies' + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_string.set("%i returned." % response.code) + return + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Request error."))) + elif error.code == 401: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Authorization error."))) + elif error.code == 403: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Permissions error."))) + elif error.code == 404: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Resource not found."))) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0]))) + else: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Generic error."))) + + sys.exit() + except urllib2.URLError, error: + tkMessageBox.showerror("Error", "Error contacting JAMF server.") + sys.exit() + except Exception as exception_message: + tkMessageBox.showerror("Error", ("Error querying Jamf. [%s]" % exception_message)) + sys.exit() + + tmp_policies = [] + for item in response_json['policies']: + # + # communicate with Jamf server + try: + url = self.jamf_hostname + '/JSSResource/policies/id/'+ str(item['id']) +'/subset/general&scope' + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Request error."))) + elif error.code == 401: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Authorization error."))) + elif error.code == 403: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Permissions error."))) + elif error.code == 404: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Resource not found."))) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0]))) + else: + tkMessageBox.showerror("Error", ("HTTP code %i: %s " % (error.code, "Generic error."))) + + sys.exit() + except urllib2.URLError, error: + tkMessageBox.showerror("Error", "Error contacting JAMF server.") + sys.exit() + except Exception as exception_message: + tkMessageBox.showerror("Error", ("Error querying Jamf. [%s]" % exception_message)) + sys.exit() + + tmp_name = response_json['policy']['general']['name'] + tmp_id = response_json['policy']['general']['id'] + tmp_all = response_json['policy']['scope']['all_computers'] + + tmp_cgroups = [] + for subpolicy in response_json['policy']['scope']['computer_groups']: + tmp_cgroups.append(subpolicy['name']) + + tmp_cs = [] + for subpolicy in response_json['policy']['scope']['computers']: + tmp_cgroups.append(subpolicy['id']) + + tmp_policies.append([tmp_name, tmp_id, tmp_all, tmp_cs, tmp_cgroups]) + + return tmp_policies + + def query_jamf_me(self): + """ + Query jamf about this particular machine + """ + + # + # this method finds the UUID for the local machine + # query's jamf and parses the ID from the record + # and then calls the main query method + # it's wasteful the first time it's called. + if not self.local_jamf_id: + + if platform.system() == 'Darwin': + local_uuid_raw = subprocess.check_output(["system_profiler", "SPHardwareDataType"]) + local_uuid = re.findall(r'Hardware UUID: (.*)', local_uuid_raw)[0] + elif platform.system() == 'Windows': + local_uuid_raw = subprocess.check_output("wmic CsProduct Get UUID") + local_uuid_raw = local_uuid_raw.split("\r\r\n")[1] + local_uuid = local_uuid_raw.split(" ")[0] + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Missing native UUID discovery.") + return + + # + # communicate with Jamf server + try: + url = self.jamf_hostname + '/JSSResource/computers/udid/' + local_uuid + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + self.local_jamf_id = response_json['computer']['general']['id'] + self.id_string.set(self.local_jamf_id) + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Request error.")) + elif error.code == 401: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Authorization error.")) + elif error.code == 403: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Permissions error.")) + elif error.code == 404: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource not found.")) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0])) + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Generic error.")) + + return + except urllib2.URLError, error: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error contacting JAMF server.") + return + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying Jamf. [%s]" % exception_message) + return + + else: + self.id_string.set(self.local_jamf_id) + + self.query_jamf_id() + + def query_jamf_id(self): + """ + Query jamf about other machine + """ + + # + # requests full record from Jamf for a specific computer + # call display method and pass record + self.reset_display() + + if not self.id_string.get(): + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("No JAMF ID set.") + return + else: + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Querying Jamf ID %s." % self.id_string.get()) + + # + # communicate with Jamf server + try: + url = self.jamf_hostname + '/JSSResource/computers/id/' + self.id_string.get() + + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Request error.")) + elif error.code == 401: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Authorization error.")) + elif error.code == 403: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Permissions error.")) + elif error.code == 404: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource not found.")) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0])) + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Generic error.")) + + return + except urllib2.URLError, error: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error contacting JAMF server.") + return + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying Jamf. [%s]" % exception_message) + return + + self.display_info(response_json) + + def display_info(self, response_json): + """ + format and display data in fields + """ + + # + # performs the actual presentaion of data to the UI + try: + locale.setlocale(locale.LC_ALL, 'en_US.utf8') + except: + pass + + # + # clear large display columns + self.ea_field.delete('1.0', END) + self.printer_field.delete('1.0', END) + self.group_field.delete('1.0', END) + self.package_field.delete('1.0', END) + self.jamf_profiles_field.delete('1.0', END) + self.jamf_policies_field.delete('1.0', END) + + # + # set StringVars + self.computer_name_string.set(response_json['computer']['general']['name']) + self.id_string.set(response_json['computer']['general']['id']) + self.fullname_string.set(response_json['computer']['location']['real_name']) + + if response_json['computer']['general']['last_contact_time']: + self.checkin_display.config(font=('', 12, 'bold')) + self.checkin_string.set(response_json['computer']['general']['last_contact_time']) + else: + self.checkin_display.config(font=('', 12, 'normal italic')) + self.checkin_string.set('No value') + + # + # parse and display printers + raw_printers = response_json['computer']['hardware']['mapped_printers'] + processed_printers = [] + for item in raw_printers: + processed_printers.append(item['name']) + for item in sorted(processed_printers, cmp=locale.strcoll, reverse=True): + self.printer_field.insert('1.0', item + "\n") + + # + # parse and display computer groups + raw_groups = response_json['computer']['groups_accounts']['computer_group_memberships'] + fmt_groups = [] + for item in raw_groups: + fmt_groups.append([item.lower(), item]) + + for item in sorted(fmt_groups, reverse=True): + self.group_field.insert('1.0', item[1] + "\n") + + # + # parse and display EA's + # + # initial pass + # retain lower case name (to sort), name and value + # + # display pass + # handle empty values as "No value" + raw_eas = response_json['computer']['extension_attributes'] + fmt_eas = [] + for item in raw_eas: + fmt_eas.append([item['name'].lower(), item['name'], item['value']]) + + for item in sorted(fmt_eas, reverse=True): + self.ea_field.tag_configure("BOLD", font='monoco 12 bold') + self.ea_field.tag_configure("NORM", font='monoco 12 normal') + self.ea_field.tag_configure("ITAL", font='monoco 12 italic') + + if "\n" in item[2]: + self.ea_field.insert('1.0', ":" + str(item[2]), ('NORM')) + elif not item[2]: + self.ea_field.insert('1.0', "No value\n", ('ITAL')) + self.ea_field.insert('1.0', ":", ('NORM')) + else: + self.ea_field.insert('1.0', ":" + str(item[2]) + "\n", ('NORM')) + + self.ea_field.insert('1.0', item[1], ('BOLD')) + + + # + # parse and display profiles + # configuration_profiles section only includes ID's, no useable names + # with list of ID's + # consult previously generated dictionary for names + raw_profiles = response_json['computer']['configuration_profiles'] + for item in sorted(raw_profiles, reverse=True): + try: + self.jamf_profiles_field.insert('1.0', self.jamf_profiles[item["id"]] + "\n") + except: + pass + + # + # parse and display installed software + raw_packages = response_json['computer']['software'] + self.package_field.tag_configure("BOLD", font='monoco 12 bold') + self.package_field.tag_configure("NORM", font='monoco 12 normal') + + for item in sorted(raw_packages['installed_by_installer_swu'], cmp=locale.strcoll, reverse=True): + self.package_field.insert('1.0', ':' + item + '\n', ('NORM')) + self.package_field.insert('1.0', 'Installer', ('BOLD')) + + for item in sorted(raw_packages['installed_by_casper'], cmp=locale.strcoll, reverse=True): + self.package_field.insert('1.0', ':' + item + '\n', ('NORM')) + self.package_field.insert('1.0', 'Casper', ('BOLD')) + + # + # parse and display policies + # + # parsing pass + # parse previously generate list of policies + # if the policy applies to all computers add it's name to new list + # if the current jamf ID appears in the list of specific computer the policy applies to, add the name to the list + # if one of the groups the computer belongs to appears in the list of groups the policy applies to, add it to the list + # + # sort and display the list. + valid_policies = [] + for item in self.jamf_policies: + if item[2] == 'True': + if item[0] not in valid_policies: + valid_policies.append[item[0]] + else: + if int(self.id_string.get()) in item[3]: + if item[0] not in valid_policies: + valid_policies.append[item[0]] + + for subitem in raw_groups: + if subitem in item[4]: + if item[0] not in valid_policies: + valid_policies.append(item[0]) + + fmt_policies = [] + for item in valid_policies: + fmt_policies.append([item.lower(), item]) + + + for item in sorted(fmt_policies, reverse=True): + self.jamf_policies_field.insert('1.0', item[1] + "\n") + + def reset_display(self): + """ + erase field contents + """ + self.computer_name_string.set("") + self.fullname_string.set("") + self.checkin_string.set("") + + self.ea_field.delete('1.0', END) + self.printer_field.delete('1.0', END) + self.group_field.delete('1.0', END) + self.package_field.delete('1.0', END) + self.jamf_profiles_field.delete('1.0', END) + self.jamf_policies_field.delete('1.0', END) + + +def login(): + """ + if the user has proper privleges, consider them an authorized user and proceed + """ + def try_login(): + """ + jamf api call for login test + """ + try: + url = jamf_hostname.get() + '/JSSResource/accounts/username/' + jamf_username.get() + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(jamf_username.get() + ':' + jamf_password.get())) + + response = urllib2.urlopen(request) + + if response.code != 200: + tkMessageBox.showerror("Jamf login", "Invalid response from Jamf") + root.destroy() # clean up after yourself! + sys.exit() + + response_json = json.loads(response.read()) + + # + # store list of user privileges + user_privileges = response_json['account']['privileges']['jss_objects'] + + # + # stop number of require privileges + count_privileges = len(required_privileges) + + # + # for every required privilege + # check if it's in user privileges + # decrement if yes + for item in required_privileges: + if item in user_privileges: + count_privileges -= 1 + + # + # if all require privileges accounted for, proceed + # else alert and fail + if count_privileges == 0: + root.destroy() # clean up after yourself! + return + else: + tkMessageBox.showerror("Jamf login", "User lacks appropriate privileges.") + + except: + tkMessageBox.showerror("Jamf login", "Invalid username or password.") + sys.exit() + + sys.exit() + + # + # This is really important. This list contains the required rights. + required_privileges = ['Read Accounts', 'Read Computer Extension Attributes', 'Read Computers', 'Read OS X Configuration Profiles', 'Read Policies'] + + root = Tk() + jamf_username = StringVar() + jamf_password = StringVar() + jamf_hostname = StringVar() + + # customizable for specific deployment + jamf_hostname.set("https://your.jamf.server:8443") + + # + # build and display login screen + root.title("Jamf Login") + mainframe = ttk.Frame(root) + mainframe.grid(column=0, row=0, sticky=NSEW) + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + root.geometry('+0+0') + + ttk.Label(mainframe, text="Jamf Server:").grid(column=1, row=10, sticky=E) + uname_entry = ttk.Entry(mainframe, width=30, textvariable=jamf_hostname) + uname_entry.grid(column=2, row=10, sticky=EW) + + ttk.Label(mainframe, text="Username:").grid(column=1, row=20, sticky=E) + uname_entry = ttk.Entry(mainframe, width=30, textvariable=jamf_username) + uname_entry.grid(column=2, row=20, sticky=EW) + + ttk.Label(mainframe, text="Password:").grid(column=1, row=30, sticky=E) + pword_entry = ttk.Entry(mainframe, width=30, textvariable=jamf_password, show="*") + pword_entry.grid(column=2, row=30, sticky=EW) + + ttk.Button(mainframe, text="Quit", command=sys.exit).grid(column=2, row=70, padx=3) + ttk.Button(mainframe, text="Login", default='active', command=try_login).grid(column=2, row=70, padx=3, sticky=E) + + if platform.system() == 'Darwin': + tmpl = 'tell application "System Events" to set frontmost of every process whose unix id is {} to true' + script = tmpl.format(os.getpid()) + output = subprocess.check_call(['/usr/bin/osascript', '-e', script]) + + root.bind('', lambda event: try_login()) + + uname_entry.focus() + root.mainloop() + + return (jamf_hostname.get(), jamf_username.get(), jamf_password.get()) + +def main(): + + jamf_hostname, jamf_username, jamf_password = login() + if not jamf_username: + sys.exit(0) + + main_window = Tk() + my_app = Summarize(main_window, jamf_hostname, jamf_username, jamf_password) + main_window.mainloop() + +if __name__ == '__main__': + main() diff --git a/cargo_ship/cargo_ship_pc.zip b/cargo_ship/cargo_ship_pc.zip new file mode 100644 index 0000000..3e4628c Binary files /dev/null and b/cargo_ship/cargo_ship_pc.zip differ diff --git a/cargo_ship/imgs/cargoship_usage.png b/cargo_ship/imgs/cargoship_usage.png new file mode 100644 index 0000000..737aed7 Binary files /dev/null and b/cargo_ship/imgs/cargoship_usage.png differ diff --git a/cargo_ship/imgs/jss_privs.png b/cargo_ship/imgs/jss_privs.png new file mode 100644 index 0000000..f260014 Binary files /dev/null and b/cargo_ship/imgs/jss_privs.png differ diff --git a/cargo_ship/imgs/login.png b/cargo_ship/imgs/login.png new file mode 100644 index 0000000..c37959b Binary files /dev/null and b/cargo_ship/imgs/login.png differ diff --git a/cargo_ship/imgs/search_results.png b/cargo_ship/imgs/search_results.png new file mode 100644 index 0000000..1991573 Binary files /dev/null and b/cargo_ship/imgs/search_results.png differ diff --git a/cargo_ship/setup_mac.py b/cargo_ship/setup_mac.py new file mode 100755 index 0000000..bab862e --- /dev/null +++ b/cargo_ship/setup_mac.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +This is a setup.py script generated by py2applet + +Usage: + python setup.py py2app +""" + +from setuptools import setup + +APP = ['cargo_ship.py'] +APP_NAME = "Cargo Ship" +DATA_FILES = [] +OPTIONS = {'argv_emulation': True, + 'iconfile': 'cargo_ship.icns', + 'plist': { + 'CFBundleName': APP_NAME, + 'CFBundleDisplayName': APP_NAME, + 'CFBundleGetInfoString': "Put your stuff into JAMF", + 'CFBundleIdentifier': "edu.utah.scl.cargoship", + 'CFBundleVersion': "1.0.0", + 'CFBundleShortVersionString': "1.0.0", + 'NSHumanReadableCopyright': u"Copyright © 2017, University of Utah, All Rights Reserved" + } + +} + +setup( + app=APP, + data_files=DATA_FILES, + options={'py2app': OPTIONS}, + setup_requires=['py2app'], +) diff --git a/cargo_ship/setup_win.py b/cargo_ship/setup_win.py new file mode 100755 index 0000000..afa2740 --- /dev/null +++ b/cargo_ship/setup_win.py @@ -0,0 +1,22 @@ +import sys +from cx_Freeze import setup, Executable + +# Dependencies are automatically detected, but it might need fine tuning. +build_exe_options = {"packages": ["os"], "excludes": ["tkinter"]} + +# GUI applications require a different base on Windows (the default is for a +# console application). +base = None +if sys.platform == "win32": + base = "Win32GUI" + +target = Executable( + script="cargo_ship.py", + base="Win32GUI", + icon="tugboat_icon.ico" + ) +setup( name = "Cargo Ship", + version = "1.0.0", + description = "Put your stuff into JAMF!", +# options = {"build_exe": build_exe_options}, + executables = [Executable("cargo_ship.py", base=base)]) diff --git a/imgs/cargoship_final.png b/imgs/cargoship_final.png new file mode 100644 index 0000000..2fc5b4a Binary files /dev/null and b/imgs/cargoship_final.png differ diff --git a/imgs/tugboat_final.png b/imgs/tugboat_final.png new file mode 100644 index 0000000..2ee64a1 Binary files /dev/null and b/imgs/tugboat_final.png differ diff --git a/tugboat/README.md b/tugboat/README.md new file mode 100644 index 0000000..84558d0 --- /dev/null +++ b/tugboat/README.md @@ -0,0 +1,230 @@ +# Tugboat + +![](../imgs/tugboat_final.png) + +Tugboat is designed to make modifications to specific computer records on your Jamf Pro server. It can read from Jamf Pro directly with specific computer ID's or by selecting from search results. + +This version is highly modified from our internal version. Please see the following discussion for advice on modifying Tugboat to best suit your needs. + +*Note: Offering a tool designed from the outset to be customized at the source level is a new venture for us. If you feel the documentation we've included is not sufficient to help you successfully navigate the source code, please consider raising an issue and we'll do our best to correct any short comings.* + +## Contents + +- [Download](#download) - get the .dmg + +- [Contact](#contact) + +- [System Requirements](#system-requirements) + +- [Install](#install) + +- [Uninstall](#uninstall) + +- [Purpose](#purpose) + +- [Usage](#usage) + - [Launching](#launching) + - [Main Window](#main-window) + - [Navigation](#navigation) + - [General](#general) + - [User and Location](#user-and-location) + - [Administration](#administration) + + +- [Jamf user privileges](#jamf-user-privileges) + +- [Customizing Tugboat](#customizing-tugboat) + + - [ldap_object.py](#ldap-object.py) + - [database_function.py](#database-funtion.py) + +- [Notes](#notes) + +- [Update History](#update-history) + + + +## Download + +[Download the latest installer here!](../../releases/) + + + +## Contact + +If you have any comments, questions, or other input, either [file an issue](../../issues) or [send us an email](mailto:mlib-its-mac-github@lists.utah.edu). Thanks! + + + +## System Requirements + +- Python 2.7+ (which you can download [here](https://www.python.org/download/)) +- Pexpect 3.3+ (which you can download [here](https://github.com/pexpect/pexpect)) + +If you intend to rebuild customized versions you will need the following tools, depending on your platform: + +- py2app for MacOS applications (which you can download [here](https://pythonhosted.org/py2app/install.html)) +- cx_freeze for Windows applications (which you can download [here](https://anthony-tuininga.github.io/cx_Freeze/)) + + + +## Install + +Place the application in an appropriate location for your environment. + + + +## Uninstall + +Remove the application. + + + +## Purpose + +Tugboat began it's life as a technology demo, something that showed what we might be able to do with the Jamf API. It focused on filling in the fields of the **User and Location** pane of a Jamf computer record. It rapidly grew into an *micro-Jamf-remote-console*. It is primarily used as a client for our inventory efforts in the Marriott Library. The following screenshot shows it's current form: + +![](imgs/internal_tugboat.png) + +We use it to onboard (completing a machines inventory data) and offboard (removing machines from management quotas) computers and is cross-platform. The version included in this repository has had all of our site-specific code removed, since those segements would have little use to any other institution. + +## Usage + +Here's a quick demonstration of Tugboat in action: +![](imgs/tugboat_in_action.gif) + +*Note: Tugboat does **not** enroll clients. To modify a computer record for a specific machine, it must already be present in the Jamf databse.* + +### Launching + +On opening the application, provide your Jamf Pro server address, and a user with appropriate privileges. + +![](imgs/login.png) + +### Main Window + +Here is the UI for the version of Tugboat included in this repository: + +![](imgs/tugboat_ui.png) + +The interface can be broken down into 4 areas: Navigation, General, User and Location, and Administration. + +### Navigation + +The first line in the UI contains tools used to select the computer record and help select the user you would like to work on. + +![](imgs/discovery.png) + +**This Device**: Open the record for the device the application is running on. + +**Search Jamf**: Enter the information you'd like to search for in the text field and press the Search Jamf button. The following image shows the search results window. + +![](imgs/search_results.png) + +Select the machine you'd like to see and it's record will display. + +**Jamf ID**: If you know the Jamf ID of the specific machine you'd like to see, enter it in the text field and press the Search Jamf button. *Note: The search string field must be empty to search for a specific ID.* + +**Top User**: Attempts to find the user who is logged into the machine the most often. It requires an administrator password to function properly. The source is left as is, and can be modified to remove specific administrator accounts, etc as needed. + +![](imgs/user_selection.png) + +If the application is able to find a valid user, it will place the account name in the Username field and displays a message in the status field at the bottom of the window. For example, on my laptop I'm logged in 99% of the time. + +![](imgs/percent_user.png) + +If this method is not useful in your environment, you can remove it from the UI by editing the source code. + +### General + +This section shows fields located in the **General** pane of the computer record. + +![](imgs/computer.png) + +The **Computer Name** can be edited and appropriate **Asset Tag** and **Bar Code** information added, if your installation makes use of them. Note: there is a second Bar Code field available in Jamf and could be added here. + +### User and Location + +This section shows the fields included in the **User and Location** pane of the computer record. If your installation offers a staff database of some form, these fields could be automatically filled in. + +![](imgs/user_and_location.png) + +Most of these fields are self-explanatory. **Department** and **Building** are special cases. These fields are maintained in the Settings on Jamf: **Network Organization**, **Buildings** and **Departments**. You may not be making use of these fields and can be ignored. If you are using them, the application will fetch the choices and make them available. + +### Administration + +The last section offers advanced tools for interacting with the Jamf web interface and submitting changes back to the Jamf database. + +![](imgs/administration.png) + +**Open in Jamf**: These buttons will open the current device, current user or current search in the Jamf web interface in your default browser. + +**Managed by Jamf**: This button is an indicator showing wether the current machine is being managed by Jamf. It is only an indicator and isn't used to toggle this value in the computer record. However, this functionality can be enabled in the source code. + +**Ready**: This is the status bar for the application. Important messages, errors and other notes will appear here to inform you about the success or failure of your activities. + +**Reset**: Reset the state of the interface. If you've made a mistake editing a machine, press this button to erase the fields and begin again. + +**Quit**: Exit the application. + +**Modify**: Submit the current computer record to the Jamf database. A *201 Submitted* status message means the record was successfully accepted by the Jamf database. + +Here are explanations of a couple potential error messages you might see: + +**409 Resource conflict**: This message means that the record you attempted to submit contained identical information to another record existing in the Jamf database. This is a rare error and occurred most often in our environment when editing Windows machines that had identical UUIDs or MAC addresses. + +**404 Resource not found**: This messages usually means the ID for the computer record you are editing was not found in the database. It's possible you may have accidentally changed it while editing other fields. Try reloading the record from the ID field or search results, make your changes and submit again. This message may occur during other activities, searching for example. + +### Jamf user privileges + +In order to use Jamf's API, your users will need the appropriate rights to certain areas of the database. To check these settings go to the following area: **All Settings**, **System Settings**, **JSS User Accounts & Groups**. + +![](imgs/jss_privs.png) + + + +This chart shows the required privileges for Cargo Ship to operate properly: + +| Field | Create | Read | Update | Delete | Notes | +| ------------------- | :----: | :--: | :----: | :----: | :----------------------------- | +| Accounts and Groups | | ☑ | | | Needed for login functionality | +| Buildings | | ☑ | | | | +| Computers | | ☑ | ☑ | | | +| Departments | | ☑ | | | | +| Users | | ☑ | ☑ | | | + +These requirements are also included in the login method. If you make customizations, you may need to add these additional areas to the list of required privileges. + +## Customizing Tugboat + +The most obvious area for customization is utilizing an external source of information about your users. Relying on such a source can greatly reduce the possibility of typos and other mistakes when editing a computer record. In our environment we have a staff database for a majority of our users and campus LDAP server for the rest. I've included code snippets in the samples subdirectory that can help you integrate these services into your version of Tugboat. + +### ldap_object.py + +This is Python object that consumes an LDAP record for a user and makes the data available to the developer by asking specific questions of it. "Is this person a student?", "What is this persons email address?", etc. Integrating this object with your code will require familiarity with the schema used by your particular institution. + +The object as written requires underlying MacOS tools and does not rely on any additional Python modules. However, it may be helpful guiding you if you prefer to use another module for interacting with your particular LDAP. + +### database_funtion.py + +The other likely candidate for user information is a staff database of some kind. In our environment this is a mySQL database. The code I've included requires the MySQLdb module, you can find more information here: [MySQL-Python](http://mysql-python.sourceforge.net/). + +Again, this code requires knowledge of the schema used by your database administrators and they should be brought into your project discussions early due the potentially sensitive nature of this type of information. Again, you will not be able to use this code as is, it will need to be heavily modified. + +## Notes + +Formerly known by a large number of names... + +I like to think of Tugboat as the spiritual successor of my first Macintosh application, written a very long time ago. You can still download it [today](https://www.info-mac.org/viewtopic.php?f=108&t=1285), but you'd need a very old Mac to run it. + +![](imgs/angstpreview.gif) + +My heartfelt thanks to the other members of the Mac Group and the IT administration of the Marriott Library for their support, bug reports and feature requests! + + + +## Update History + +| Date | Version | Notes | +| --------- | ------- | ----------------------- | +| 2017.2.15 | 1.5.0 | Initial public release. | \ No newline at end of file diff --git a/tugboat/Tugboat application.dmg b/tugboat/Tugboat application.dmg new file mode 100644 index 0000000..12fe901 Binary files /dev/null and b/tugboat/Tugboat application.dmg differ diff --git a/tugboat/database_function.py b/tugboat/database_function.py new file mode 100644 index 0000000..c6e2dde --- /dev/null +++ b/tugboat/database_function.py @@ -0,0 +1,100 @@ +def dbase(self): + """ + sample method to parse databse info into fields useable by tugboat + """ + # + # Staff database + # + + try: + if self.valid_unid(): + pass + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error searching database, no valid uNID.") + self.reset_user() + return + + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Marriott Staff database selected.") + + try: + db = MySQLdb.connect(host="your.mysql.server", # your host, usually localhost + user="your_user", # your username + passwd="your_password", # your password + db="your_db") # name of the data base + except: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error connecting to database.") + return + + staff = db.cursor() + supervisor = db.cursor() + division = db.cursor() + department = db.cursor() + staff.execute("""SELECT name_last, name_first, division_id, department_id,email, phone, campusAddr FROM staff WHERE unid = '%s';""" % self.endusername_string.get()) + if int(staff.rowcount) == 0: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying specific staff.") + db.close() + return + else: + for row in staff.fetchall(): + self.fullname_string.set(row[1] + " " + row[0]) + my_division = row[2] + my_dept = row[3] + self.email_string.set(row[4]) + self.phone_string.set(row[5]) + self.room_string.set(row[6]) + + supervisor.execute("""SELECT supervisor_unid FROM staff_supervisors WHERE staff_unid = '%s';""" % self.endusername_string.get()) + if int(supervisor.rowcount) == 0: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying supervisor.") + db.close() + return + else: + for row in supervisor.fetchall(): + self.supervisor_endusername_string.set(row[0]) + break + + division.execute("""SELECT name FROM division WHERE id = '%s';""" % my_division) + if int(division.rowcount) == 0: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying division.") + db.close() + return + else: + for row in division.fetchall(): + division_name = row[0] + + department.execute("""SELECT name FROM department WHERE id = '%s';""" % my_dept) + if int(division.rowcount) == 0: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying department.") + db.close() + return + else: + for row in department.fetchall(): + department_name = row[0] + + db.close() + + # + # sets popup menus to correct values + self.division_string.set(division_name) + self.position_string.set(department_name) + + if inspect.stack()[1][3] == "__call__": + self.previous_unid = [] + + self.supervisor_btn.configure(state="enabled") +# self.super_down_btn.configure(state="disabled") + + return + + except ValueError: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error setting dbase Mode.") + db.close() + return diff --git a/tugboat/imgs/administration.png b/tugboat/imgs/administration.png new file mode 100644 index 0000000..c6226ba Binary files /dev/null and b/tugboat/imgs/administration.png differ diff --git a/tugboat/imgs/angstpreview.gif b/tugboat/imgs/angstpreview.gif new file mode 100755 index 0000000..2c58d52 Binary files /dev/null and b/tugboat/imgs/angstpreview.gif differ diff --git a/tugboat/imgs/computer.png b/tugboat/imgs/computer.png new file mode 100644 index 0000000..cd977b8 Binary files /dev/null and b/tugboat/imgs/computer.png differ diff --git a/tugboat/imgs/discovery.png b/tugboat/imgs/discovery.png new file mode 100644 index 0000000..9b3cf33 Binary files /dev/null and b/tugboat/imgs/discovery.png differ diff --git a/tugboat/imgs/internal_tugboat.png b/tugboat/imgs/internal_tugboat.png new file mode 100644 index 0000000..7ed17ed Binary files /dev/null and b/tugboat/imgs/internal_tugboat.png differ diff --git a/tugboat/imgs/jss_privs.png b/tugboat/imgs/jss_privs.png new file mode 100644 index 0000000..f260014 Binary files /dev/null and b/tugboat/imgs/jss_privs.png differ diff --git a/tugboat/imgs/login.png b/tugboat/imgs/login.png new file mode 100644 index 0000000..c37959b Binary files /dev/null and b/tugboat/imgs/login.png differ diff --git a/tugboat/imgs/percent_user.png b/tugboat/imgs/percent_user.png new file mode 100644 index 0000000..ff10c0a Binary files /dev/null and b/tugboat/imgs/percent_user.png differ diff --git a/tugboat/imgs/search_results.png b/tugboat/imgs/search_results.png new file mode 100644 index 0000000..1991573 Binary files /dev/null and b/tugboat/imgs/search_results.png differ diff --git a/tugboat/imgs/tugboat_in_action.gif b/tugboat/imgs/tugboat_in_action.gif new file mode 100644 index 0000000..7a3b225 Binary files /dev/null and b/tugboat/imgs/tugboat_in_action.gif differ diff --git a/tugboat/imgs/tugboat_ui.png b/tugboat/imgs/tugboat_ui.png new file mode 100644 index 0000000..310a982 Binary files /dev/null and b/tugboat/imgs/tugboat_ui.png differ diff --git a/tugboat/imgs/user_and_location.png b/tugboat/imgs/user_and_location.png new file mode 100644 index 0000000..c38b9bc Binary files /dev/null and b/tugboat/imgs/user_and_location.png differ diff --git a/tugboat/imgs/user_selection.png b/tugboat/imgs/user_selection.png new file mode 100644 index 0000000..b28712c Binary files /dev/null and b/tugboat/imgs/user_selection.png differ diff --git a/tugboat/ldap_object.py b/tugboat/ldap_object.py new file mode 100644 index 0000000..4af8e0c --- /dev/null +++ b/tugboat/ldap_object.py @@ -0,0 +1,161 @@ +class LDAP_record: + """ + consume LDAP record and provide methods for accessing interesting data + """ + def __init__(self, unid): + self.error = False + ldap_dict = {} + + # + # request complete user record from LDAP + cmd = "/Users/" + unid + try: + raw_data = subprocess.check_output(["/usr/bin/dscl", "/LDAPv3/your.ldap.server", "-read", cmd]) + except: + self.error = True + return + + # + # begin parsing data into dictionary + raw_data = string.replace(raw_data, '\n ', ' ') + raw_data = raw_data.split('\n') + + for line in raw_data: + y = line.split(":") + y = [x for x in y if 'dsAttrTypeNative' not in x] + + if len(y) == 2: + key = y[0] + value = y[1] + value = value.lstrip() + + else: + key = y[0] + value = y[1:] + value = [x for x in value if x] + + if key: + ldap_dict[key] = value + + self.record = ldap_dict + + def is_student(self): + try: + if 'CurrentStudent' in self.record['Student']: + return True + else: + return False + except: + return False + + def is_staff(self): + try: + if self.record['Employee']: return True + except: + return False + + def my_name(self): + try: + if self.record['gecos']: + if len(self.record['gecos']) > 1: + return self.record['gecos'] + else: +# print "Beep!" + try: + if self.record['displayName']: return self.record['displayName'] + except: + return None + except: + try: + if self.record['displayName']: return self.record['displayName'] + except: + return None + + def my_title(self): + try: + if self.record['title']: return self.record['title'] + except: + return None + + def my_email(self): + try: + if self.record['mail']: return self.record['mail'] + except: + try: + if self.record['ExtensionAttribute4']: return self.record['ExtensionAttribute4'] + except: + return None + + def my_phone(self): + try: + if self.record['telephoneNumber']: return self.record['telephoneNumber'] + except: + return None + + def my_department(self): + try: + if self.record['department']: return self.record['department'] + except: + return None + + def my_address(self): + try: + if self.record['streetAddress']: return self.record['streetAddress'] + except: + return None + + # + # diagnostic methods + def print_full(self): + for k, v in self.record.items(): + print ("%s > %r" % (k, v)) + + def print_keys(self): + return self.record.keys() + + + +def ldap(self): + """ + translate LDAP data from object into fields used in tugboat + """ + try: + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("LDAP selected.") + + if self.valid_unid(): + print("ldap %r" % self.endusername_string.get()) + this_person = LDAP_record(self.endusername_string.get()) + if not this_person.error: + + self.fullname_string.set(this_person.my_name()) + self.email_string.set(this_person.my_email()) + self.phone_string.set(this_person.my_phone()) + self.room_string.set(this_person.my_address()) + if this_person.my_title() is None: + if this_person.my_department() is None: + self.position_string.set("") + else: + self.position_string.set(this_person.my_department()) + else: + if this_person.my_department() is None: + self.position_string.set(this_person.my_title()) + else: + self.position_string.set(this_person.my_title() + "/" + this_person.my_department()) + if self.division_string.get(): + self.division_string.set('None') + if self.building_string.get(): + self.building_string.set('None') + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("LDAP error, no record found for uNID.") + self.reset_data() + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error setting LDAP Mode, no valid uNID.") + self.reset_user() + return + except ValueError: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error setting LDAP Mode.") + return diff --git a/tugboat/setup_mac.py b/tugboat/setup_mac.py new file mode 100755 index 0000000..b4524ce --- /dev/null +++ b/tugboat/setup_mac.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +This is a setup.py script generated by py2applet + +Usage: + python setup.py py2app +""" + +from setuptools import setup + +APP = ['tugboat.py'] +APP_NAME = "Tugboat" +DATA_FILES = [] +OPTIONS = {'argv_emulation': True, + 'iconfile': 'tugboat_icon.icns', + 'plist': { + 'CFBundleName': APP_NAME, + 'CFBundleDisplayName': APP_NAME, + 'CFBundleGetInfoString': "Put your stuff into JAMF", + 'CFBundleIdentifier': "edu.utah.scl.tugboat", + 'CFBundleVersion': "1.5.0", + 'CFBundleShortVersionString': "1.5.0", + 'NSHumanReadableCopyright': u"Copyright © 2017, University of Utah, All Rights Reserved" + } + +} + +setup( + app=APP, + data_files=DATA_FILES, + options={'py2app': OPTIONS}, + setup_requires=['py2app'], +) diff --git a/tugboat/setup_win.py b/tugboat/setup_win.py new file mode 100755 index 0000000..47e1362 --- /dev/null +++ b/tugboat/setup_win.py @@ -0,0 +1,22 @@ +import sys +from cx_Freeze import setup, Executable + +# Dependencies are automatically detected, but it might need fine tuning. +build_exe_options = {"packages": ["os"], "excludes": ["tkinter"]} + +# GUI applications require a different base on Windows (the default is for a +# console application). +base = None +if sys.platform == "win32": + base = "Win32GUI" + +target = Executable( + script="tugboat2", + base="Win32GUI", + icon="tugboat_icon2.ico" + ) +setup( name = "Tugboat", + version = "1.5.0", + description = "Put your stuff into JAMF!", + options = {"build_exe": build_exe_options}, + executables = [Executable("tugboat_pc.py", base=base)]) diff --git a/tugboat/tugboat.py b/tugboat/tugboat.py new file mode 100755 index 0000000..7bb493a --- /dev/null +++ b/tugboat/tugboat.py @@ -0,0 +1,1462 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 University of Utah Student Computing Labs. ################ +# All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appears in all copies and +# that both that copyright notice and this permission notice appear +# in supporting documentation, and that the name of The University +# of Utah not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. This software is supplied as is without expressed or +# implied warranties of any kind. +################################################################################ + +# tugboat.py ################################################# +# +# A Python Tk application to edit Jamf computer records. +# +# +# 1.5.0 2017.02.15 Initial public release. tjm +# +# +################################################################################ + +# notes: ####################################################################### +# +# py2app: +# rm -rdf build dist ; /usr/bin/python setup.py py2app -s +# +# +# +# +################################################################################ + + +from __future__ import print_function +from Tkinter import * +import tkSimpleDialog +import tkMessageBox +import ttk +import os +import re +import subprocess +import urllib +import urllib2 +import xml.etree.cElementTree as ET +from xml.dom.minidom import parseString +import socket +import platform +import base64 +import json +import webbrowser +import inspect +if platform.system() == 'Darwin': + import pexpect + +class Computer(object): + """ + Store GUI and data structures describing jamf computer records + """ + def __init__(self, root, uuid, running_under_osx, jamf_hostname, jamf_username, jamf_password): + """ + initialize variables and data structures + """ + self.root = root + self.uuid = uuid + self.jamf_hostname = jamf_hostname + self.jamf_password = jamf_password + self.jamf_username = jamf_username + self.running_under_osx = running_under_osx + self.local_jamf_id = None + + self.hostname = "" + self.divisions = [] + self.buildings = [] + self.platform = None + self.will_offboard = False + self.jamf_management = "" + + self.username_string = StringVar() + self.fullname_string = StringVar() + self.department_string = StringVar() + self.position_string = StringVar() + self.email_string = StringVar() + self.phone_string = StringVar() + self.building_string = StringVar() + self.room_string = StringVar() + self.assettag_string = StringVar() + self.barcode_string = StringVar() + self.status_string = StringVar() + self.id_string = StringVar() + self.search_string = StringVar() + self.computer_name_string = StringVar() + + self.username_string.set("") + self.fullname_string.set("") + self.position_string.set("") + self.email_string.set("") + self.phone_string.set("") + self.room_string.set("") + self.assettag_string.set("") + self.status_string.set("Ready.") + self.computer_name_string.set("") + + self.status_warning = ttk.Style() + self.status_warning.configure('Warning.TLabel', foreground='red') + + self.status_normal = ttk.Style() + self.status_normal.configure('Normal.TLabel', foreground='black') + + self.highlight_button = ttk.Style() + self.highlight_button.configure('Highlight.TButton', foreground='green') + + self.dim_text = ttk.Style() + self.dim_text.configure('Dim.TLabel', foreground='gray50') + + self.black_text = ttk.Style() + self.black_text.configure('Black.TLabel', foreground='black') + + self.status_subtle = ttk.Style() + self.status_subtle.configure('Subtle.TLabel', foreground='maroon') + + self.hostname = (socket.gethostname()).split(".")[0] + self.divisions = self.populate_menu('departments') + self.buildings = self.populate_menu('buildings') + + self.build_ui() + + + def build_ui(self): + """ + describe UI, fields, buttons, etc + """ + + # + # This is an encoded gif of the title image + self.logo_image = '''\ + R0lGODlhWAJRAPcAAAEAAAQECgYJCgwMDQgHCBMODQ4QDhERDg0OEQUHFREOEw4REgoTGhITFBQV + GhUZGxsbHBkXGCcXFR0iHiwkHRwdIxYZJhAVLyIcIxwjJRcpNiUlJSQlKyUqLCwrLCknJzQqKCgr + NjMtNSwyOTMzNTM0Ozs7PDk3ODYyK1ExFWo3Ekc3KUQ7OlY0LF8rKD9DPHdIGnBJGUpEO1ZJOVdJ + LHZLJXBRMHdhNxknQicqRS01RTM1Qjs8Qzg6Ry86VRkvWEQ9REk+TGYzQD1DSDlFVTlTY0NCQ0RD + S0xLTEtHR1NLSVhSS0hJVlJNU1ROWExTWFVSVFtUVFlZWVdVWkhTSGhXSGRaWXFQS2pkXXdoVEtV + Z1dZZlFUa2VdZFVpbmxra2dlZ3Rpamdpdnx9fXd2d3FzcFJjWaE0MMQSMMcfO8chPMgjPtQyOPow + OolXG4xRF5llHLBtGYhYJ5ZbJ4lVM5hmKYlmN5doN5NsMKhrJqd0KrZ4Kah4N7Z7NqxqN6VZKMh4 + L+BXLc46U8kqRPwxRoZnRpByT613TIt3bq51bpNYTtFGXtBFVch0TtRTaNJMY9lnevNvVXCNe1aP + bj+EbriFObmEKsiIKNmZK8WIN8qUOdiYNtGPLOSZMNujOdymMOmmK/WqKPq1KueoN+2zOfW1NPS0 + O/u6PPm5N/OsN+CYG/7FOf/MMbKLTI+Idq+PbrCrcJSRYMeWRs6QT9mlRc+nVeeqRuyzRve5RO6y + UcySbNOrbuyza+iTUv3FRfvOUPXKbv/rZHJ3hVZxkYF+hdtugN13iHiJh3ajmjOL5kiS342NjYeE + hpycnJiVl5CQjrKUjJGpl7ColJadpqqqqqWnqLi4uLa2tbGwrpqjp9CYjuKKmeGFlOyXkc2wkPiq + k8q3rPKxsOaap7XKne3Pj9TKtvLMruvZobu6w9m9xLjGyMXFxcfKydHR0d3d3dbV19LQz/DCyvnF + w+zVzObk2vjt0Nrd5+np6eXm5fj16/T09Pzz9f////f3+uzu8d3h5yH5BAAAAAAALAAAAABYAlEA + AAj+APkJHEiwoMGDCBMO1PdKRQoVgGzlojWLjgULCaj1E/KnHr93AufVoycNm8B+/PzhU8iypcuX + MGPKnEmzps2bOHPq3Mmzp8+fQIMKHSrT24owKSRQ0PML1x0WM2SU83coUDl72Oj1mzfPX7VqAvXx + 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 + oinNr6VaGMxSFhgDdbgHzzVduXKSLxBdveiH3bnY6eXeLSqGl0tdNFjdmDETEzCAoZPRU/y9+FGp + vXXXvx3F7dDU3r3bok3f4F2Ojqy23zuyjizU2nOOCFCpAAiABICAD7CC0+hezLonzfsCaF3gCEYo + 1A3fQuGLfRCHDJZePvGHCgAACKgAUpQjwXxRoYU9weQOvUW67SQIkireQZo2w83fv7Rf3dqOwIXU + GSxgZTwBLFgGbLgHlZXgLbLQitkAkR3iJE4eCobbMOr+B26ABEgoBnH4kzIAgCQQgaFTWhNmDpWi + RXatYVEc4fS1X6tJYfhlV+YIAHbNrRlMDAJOjqk9GiNghmrgPyVGKH1QU5Uxgu3F4z9OGCb+U84K + B3GIh0WIB24IBz/5h5hkmkK64W5jV6vJrdq9Xem01FJMY2sTJDiuGAImYAF4SSXoAlcogzGgQHcQ + YkBGqHtYSiMmE1aW5dUR5LfkLH4Ih3iYB0EoZCr2E2eIrg0ARfmNuquJQdqtxWMWpEqu5GUeYWVr + Zul04+RIjgJeANHUAiwQhmkYAzD44WuwhnZY5Vl+tdJohgf+AkQIDXJmZ5nxUzVQgzSQ53mm53q2 + 53v+ToMYu2V+iAdjiId42AZjmIeEAQNlewAYZY7phObpnMFaTGOGliNPXuhqvgwCtgAk+AIywEp4 + cId8QOJ2BumQbud96AcP3IeTNmmSTmmVPumWbumU9sB82OC+EIdiiIdiWOSadQACQIEISNoz5lkj + C1o5ouZqJlACnj1cxdwEiIANAAGG+AJmmAZwGGeRtuqrbmdBZZ2Z9hOKIQDuyduwbt9pRowCdtEG + mAAeaIItKBhnqAZrYAd4eId1wAZsWAd88Id8wOq95uu+Zs1GmWhlkz3nkIA6I6tnwIZrQId2oIfo + 9evHhuzINlBsYIZniAxnaIZmqIZyaAd38FjJBu0V0BZt1tTq0Tbt00bt1Fbt1WbtBQ4IACH/C1hN + UCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtj + OWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUg + WE1QIENvcmUgNS42LWMxMzggNzkuMTU5ODI0LCAyMDE2LzA5LzE0LTAxOjA5OjAxICAgICAgICAi + PgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1z + eW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIi8+CiA8L3JkZjpSREY+ + CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp + 6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGw + r66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3 + dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+ + PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYF + BAMCAQAAOw== + ''' + + self.root.title("Tugboat") + + self.mainframe = ttk.Frame(self.root) + + self.mainframe.grid(column=0, row=0, sticky=NSEW) + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + self.root.geometry('+0+0') + + # + # position and display logo image + self.logo_label = ttk.Label(self.mainframe) + self.logo_photoimage = PhotoImage(data=self.logo_image) + self.logo_label['image'] = self.logo_photoimage + self.logo_label.grid(column=1, row=0, columnspan=4) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=30, columnspan=35, sticky=EW) + + # + # these are the elements of the Navigation section of the UI + ttk.Label(self.mainframe, text="Discovery Method:").grid(column=1, row=100, sticky=E) + ttk.Button(self.mainframe, text="This Device", style='Highlight.TButton', command=self.query_jamf_me).grid(column=2, row=100, sticky=W) + + ttk.Button(self.mainframe, text="Search Jamf", command=self.search_string_jamf).grid(column=2, row=100, sticky=E) + self.search_entry = ttk.Entry(self.mainframe, width=25, textvariable=self.search_string) + self.search_entry.grid(column=3, row=100, columnspan=2, sticky=W) + + ttk.Label(self.mainframe, text="Jamf ID: ").grid(column=4, row=100, sticky=E) + self.id_entry = ttk.Entry(self.mainframe, width=6, textvariable=self.id_string) + self.id_entry.grid(column=4, row=100, sticky=E) + + if self.running_under_osx: + ttk.Label(self.mainframe, text="User Selection:").grid(column=1, row=150, sticky=E) + ttk.Button(self.mainframe, text="Top User (Admin Req.)", command=self.usage).grid(column=2, row=150, sticky=W) + + # + # If you are considering adding UI elements to communicate with user database, place them here + # + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=300, columnspan=5, sticky=EW) + + # + # these are the elements of the General section of the UI + ttk.Label(self.mainframe, text="Computer Name:").grid(column=1, row=320, sticky=E) + self.computername_entry = ttk.Entry(self.mainframe, textvariable=self.computer_name_string) + self.computername_entry.grid(column=2, row=320, sticky=EW) + + ttk.Label(self.mainframe, text="Asset Tag:").grid(column=1, row=340, sticky=E) + self.assettag_entry = ttk.Entry(self.mainframe, textvariable=self.assettag_string) + self.assettag_entry.grid(column=2, row=340, sticky=EW) + + ttk.Label(self.mainframe, text="Bar Code:").grid(column=3, row=340, sticky=E) + self.barcode_entry = ttk.Entry(self.mainframe, textvariable=self.barcode_string) + self.barcode_entry.grid(column=4, row=340, sticky=EW) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=500, columnspan=5, sticky=EW) + + # + # these are the elements of the User and Location section of the UI + ttk.Label(self.mainframe, text="Username:").grid(column=1, row=550, sticky=E) + self.endusername_entry = ttk.Entry(self.mainframe, textvariable=self.username_string) + self.endusername_entry.grid(column=2, row=550, sticky=EW) + + ttk.Label(self.mainframe, text="Full Name:").grid(column=3, row=550, sticky=E) + self.fullname_entry = ttk.Entry(self.mainframe, width=31, textvariable=self.fullname_string) + self.fullname_entry.grid(column=4, row=550, sticky=EW) + + ttk.Label(self.mainframe, text="Department:").grid(column=1, row=600, sticky=E) + self.division_combobox = ttk.Combobox(self.mainframe, width=31, state="readonly", textvariable=self.department_string) + self.division_combobox['values'] = self.divisions + self.division_combobox.current(0) + self.division_combobox.grid(column=2, row=600, sticky=EW) + + ttk.Label(self.mainframe, text="Position:").grid(column=3, row=600, sticky=E) + self.position_entry = ttk.Entry(self.mainframe, width=31, textvariable=self.position_string) + self.position_entry.grid(column=4, row=600, sticky=EW) + + ttk.Label(self.mainframe, text="Email:").grid(column=1, row=650, sticky=E) + self.email_entry = ttk.Entry(self.mainframe, width=31, textvariable=self.email_string) + self.email_entry.grid(column=2, row=650, sticky=EW) + + ttk.Label(self.mainframe, text="Phone:").grid(column=3, row=650, sticky=E) + self.phone_entry = ttk.Entry(self.mainframe, width=31, textvariable=self.phone_string) + self.phone_entry.grid(column=4, row=650, sticky=EW) + + ttk.Label(self.mainframe, text="Building:").grid(column=1, row=700, sticky=E) + self.building_combobox = ttk.Combobox(self.mainframe, width=31, state="readonly", textvariable=self.building_string) + self.building_combobox['values'] = self.buildings + self.building_combobox.current(0) + self.building_combobox.grid(column=2, row=700, sticky=EW) + + ttk.Label(self.mainframe, text="Room:").grid(column=3, row=700, sticky=E) + self.room_entry = ttk.Entry(self.mainframe, width=31, textvariable=self.room_string) + self.room_entry.grid(column=4, row=700, sticky=EW) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=800, columnspan=5, sticky=EW) + + # + # these are the elements of the Administration section of the UI + ttk.Label(self.mainframe, text="Open in Jamf:").grid(column=1, row=850, sticky=E) + ttk.Button(self.mainframe, text="Device", command=self.open_id_web).grid(column=2, row=850, sticky=W) + ttk.Button(self.mainframe, text="User", command=self.open_user_web).grid(column=2, row=850) + ttk.Button(self.mainframe, text="Search", command=self.open_search_web).grid(column=2, row=850, sticky=E) + + self.jamf_management_label = ttk.Label(self.mainframe, text="Managed by Jamf: ") + self.jamf_management_label.grid(column=4, row=850, sticky=E) + self.jamf_management_btn = ttk.Button(self.mainframe, text="True", width=6, command=lambda: self.jamf_management_btn.config(text="False") if self.jamf_management_btn.config('text')[-1] == 'True' else self.jamf_management_btn.config(text="True")) + self.jamf_management_btn.grid(column=4, row=850, sticky=E) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=1000, columnspan=5, sticky=EW) + + self.status_label = ttk.Label(self.mainframe, textvariable=self.status_string) + self.status_label.grid(column=1, row=1100, sticky=W, columnspan=4) + + ttk.Button(self.mainframe, text="Reset", width=6, command=self.reset_data).grid(column=4, row=1100, sticky=W) + ttk.Button(self.mainframe, text="Quit", width=6, command=self.root.destroy).grid(column=4, row=1100) + + self.submit_btn = ttk.Button(self.mainframe, text="Modify", width=7, command=self.submit) + self.submit_btn.grid(column=4, row=1100, sticky=E) + + # + # this loop adds a small amount of space around each UI element, changing the value will significantly change the final size of the window + for child in self.mainframe.winfo_children(): + child.grid_configure(padx=3, pady=3) + + def open_user_web(self): + """ + Open currently displayed user record in jamf + """ + + # + # in order to open the user in a browser you need the user's Jamf ID + # in order to get the ID you need to open the user's record on Jamf + if not self.username_string.get(): + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("No user available.") + return + + url = self.jamf_hostname+ '/JSSResource/users/name/' + self.username_string.get() + url = urllib.quote(url, ':/()') + + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + jamf_user_id = response_json['user']['id'] + + if jamf_user_id: + url_formatted = self.jamf_hostname+ "/users.html?id=" + str(jamf_user_id) + "&o=r" + webbrowser.open_new_tab(url_formatted) + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Opened URL for User.") + + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("No user available.") + + def open_id_web(self): + """ + Open currently displayed computer record in jamf + """ + if self.id_string.get(): + url_formatted = self.jamf_hostname+ "/computers.html?id=" + self.id_string.get() + "&o=r" + webbrowser.open_new_tab(url_formatted) + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Opened URL for ID.") + + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("No ID available.") + + def open_search_web(self): + """ + Open currently displayed search in jamf + """ + if self.search_string.get(): + url_formatted = self.jamf_hostname+ "/computers.html?queryType=Computers&query=*" + self.search_string.get() + "*" + webbrowser.open_new_tab(url_formatted) + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Opened URL for search.") + + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("No search string entered.") + + def reset_user(self): + """ + Reset user data structures to blank + """ + + self.username_string.set("") + self.fullname_string.set("") + self.department_string.set("") + self.position_string.set("") + self.email_string.set("") + self.phone_string.set("") + self.building_string.set("") + self.room_string.set("") + + def reset_data(self): + """ + reset all data structures to blank + """ + + if inspect.stack()[1][3] == "__call__": + self.id_string.set("") + self.search_string.set("") + + self.computer_name_string.set("") + self.assettag_string.set("") + self.barcode_string.set("") + self.username_string.set("") + self.fullname_string.set("") + self.department_string.set("") + self.position_string.set("") + self.email_string.set("") + self.phone_string.set("") + self.building_string.set("") + self.room_string.set("") + + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Data reset.") + + def display_user(self): + """ + Print current data structures, useful for debugging + """ + print("Current user information") + print("\tGeneral") + print("\t\tComputer name : %s" % self.computer_name_string.get()) + print("\t\tAsset Tag : %s" % self.assettag_string.get()) + print("\t\tBar code : %s\n" % self.barcode_string.get()) + + print("\tUser and Location") + print("\t\tEnduser : %s" % self.username_string.get()) + print("\t\tFullname : %s" % self.fullname_string.get()) + print("\t\tDepartment : %s" % self.department_string.get()) + print("\t\tPosition : %s" % self.position_string.get()) + print("\t\tEmail : %s" % self.email_string.get()) + print("\t\tPhone : %s" % self.phone_string.get()) + print("\t\tBuilding : %s" % self.building_string.get()) + print("\t\tRoom : %s\n" % self.room_string.get()) + + print("Other :") + print("\tJamf ID : %s" % self.id_string.get()) + print("\tStatus : %s\n" % self.status_string.get()) + + return + + def check_submit(self): + """ + precheck required fields for valid content + """ + + # + # if you plan on adding additional fields, you'll likely want to add them to this + # method to be sure they contain valid values before submitting. + bad_fields = [] + +# if not self.assettag_string.get(): +# bad_fields.append("Asset Tag") + + if not self.id_string.get(): + bad_fields.append("Jamf ID") + + if not self.username_string.get(): + bad_fields.append("Username") + + if not self.fullname_string.get(): + bad_fields.append("Full Name") + + if not bad_fields: + # We're good. + return True + else: + if len(bad_fields) >= 5: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Many empty fields!") + return False + else: + bad_fields = ", ".join(bad_fields) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Empty fields: %s" % bad_fields) + return False + + def submit(self): + """ + submit current data structures to jamf + """ + + if not self.check_submit(): + return + + try: + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Submitting...") + + jamf_url = self.jamf_hostname+ '/JSSResource/computers/id/' + self.id_string.get() + + # + # These are the individual fields associated with UI elements + # If you add additional fields to the UI, you will need to add corresponding + # XML elements form them to be submitted back to the Jamf database. + top = ET.Element('computer') + + general = ET.SubElement(top, 'general') + + computer_name_xml = ET.SubElement(general, 'name') + computer_name_xml.text = self.computer_name_string.get() + + asset_tag_xml = ET.SubElement(general, 'asset_tag') + asset_tag_xml.text = self.assettag_string.get() + + barcode_xml = ET.SubElement(general, 'barcode_1') + barcode_xml.text = self.barcode_string.get() + + location = ET.SubElement(top, 'location') + + username_xml = ET.SubElement(location, 'username') + username_xml.text = self.username_string.get() + + email_xml = ET.SubElement(location, 'email_address') + email_xml.text = self.email_string.get() + + realname_xml = ET.SubElement(location, 'real_name') + realname_xml.text = self.fullname_string.get() + + phone_xml = ET.SubElement(location, 'phone') + phone_xml.text = self.phone_string.get() + + building_xml = ET.SubElement(location, 'building') + building_xml.text = self.building_string.get() + + room_xml = ET.SubElement(location, 'room') + room_xml.text = self.room_string.get() + + position_xml = ET.SubElement(location, 'position') + position_xml.text = self.position_string.get() + + department_xml = ET.SubElement(location, 'department') + department_xml.text = self.department_string.get() + + # + # these are the fields that enable removing machines + # from management quotas. +# if self.jamf_management_btn.config('text')[-1] == 'True' +# remote_management = ET.SubElement(general, 'remote_management') +# managed_xml = ET.SubElement(remote_management, 'managed') +# managed_xml.text = 'true' +# else +# remote_management = ET.SubElement(general, 'remote_management') +# managed_xml = ET.SubElement(remote_management, 'managed') +# managed_xml.text = 'false' + + +# print(ET.tostring(top)) + + # + # comminicating with the Jamf database and putting the XML structure + opener = urllib2.build_opener(urllib2.HTTPHandler) + request = urllib2.Request(jamf_url, data=ET.tostring(top)) + base64string = base64.b64encode('%s:%s' % (self.jamf_username, self.jamf_password)) + request.add_header("Authorization", "Basic %s" % base64string) + request.add_header('Content-Type', 'text/xml') + request.get_method = lambda: 'PUT' + response = opener.open(request) + + self.status_label.configure(style='Normal.TLabel') + self.status_string.set(str(response.code) + " Submitted.") + return + + except urllib2.HTTPError, error: + if error.code == 400: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Request error.")) + elif error.code == 401: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Authorization error.")) + elif error.code == 403: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Permissions error.")) + elif error.code == 404: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource not found.")) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0])) + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Generic error.")) + + return + except urllib2.URLError, error: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error contacting Jamf.") + return + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error submitting to Jamf. [%s]" % exception_message) + return + + def usage(self): + """ + Calculate which valid user uses this computer the most + """ + # + # this method uses the pexpect module, if you have issues with the module you'll need to remove this method. + try: + # + # aquire admin password + password = tkSimpleDialog.askstring("Password", "Enter admin password:", show='*', parent=self.root) + + if not password: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Canceled Top User.") + return + + cmd_output = [] + try: + # + # utilizing the + child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/ac -p']) + + exit_condition = False + while not exit_condition: + result = child.expect(['\n\nPass', 'Password:', 'sudo', pexpect.EOF, pexpect.TIMEOUT]) + + cmd_output.append(child.before) + cmd_output.append(child.after) + if result == 0: + child.sendline(password) + elif result == 1: + child.sendline(password) + elif result == 2: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Incorrect admin password.") + return + elif result == 3: + exit_condition = True + elif result == 4: + exit_condition = True + else: + print("Unknown error. Exiting.") + exit_condition = True + + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error submitting to Jamf. [%s]" % exception_message) + return + + # + # begin parsing out useful content + checked_output = [] + for index, content in enumerate(cmd_output): + if isinstance(content, basestring): + if "System\r\nAdministrator." in content: + pass + elif not content: + pass + elif content == "Password:": + pass + else: + checked_output.append(content) + + # + # begin parsing out users and time data from output + users_raw = [] + for super_item in checked_output: + sub_item = super_item.split("\r\n") + for user in sub_item: + if user: + user = user.strip() + users_raw.append(user) + + users = [item for item in users_raw if item] + + grid = {} + + # + # build dictionary of output + for item in users: + match = re.search(r'^(\w*)\s*(\d*.\d*)', item) + grid[match.group(1)] = match.group(2) + + login_total = float(grid['total']) + + # + # remove various admin/management accounts + try: + del grid['total'] + except Exception: + pass + + try: + del grid['admin'] + except Exception: + pass + + try: + del grid['root'] + except Exception: + pass + + try: + del grid['_mbsetupuser'] + except Exception: + pass + + try: + del grid['radmind'] + except Exception: + pass + + try: + del grid['Guest'] + except Exception: + pass + + # + # calculate time percentages of remaining users + for item in grid: + temp = float(grid[item]) + grid[item] = temp / login_total + + # sort the remaining users, based on percentage + grid_sorted = sorted(grid, key=grid.__getitem__, reverse=True) + + # + # if there are still valid users, continue + if grid_sorted: + + # + # select the top user. + self.username_string.set(grid_sorted[0]) + high_user_percentge = int(grid[grid_sorted[0]]* 100) + self.status_label.configure(style='Normal.TLabel') + self.status_string.set(str(high_user_percentge) + "% user selected.") + return + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error selecting highest usage user, no eligible users") + return + + except ValueError: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error setting Usage Mode.") + return + + def button_state(self): + """ + sets buttons to the correct state + """ + + # + # This method is used to enable/disable specific buttons, based on OS, etc. + # + # You want to use the Combobox option of state='disabled'. + # + # There are three options for state as follows: + # + # state='normal' which is the fully functional Combobox. + # state='readonly' which is the Combobox with a value, but can't be changed (directly). + # state='disabled' which is where the Combobox cannot be interacted with. + # + # self.highlight_button = ttk.Style() + # self.highlight_button.configure('Highlight.TButton', foreground='red') + # ttk.Button(self.mainframe, text="Query this system", style='Highlight.TButton', command= lambda: self.query_jamf_me()).grid(column=2, row=20, padx =3, sticky=W) + + if self.platform == "Mac": + self.jamf_management_btn.configure(state="normal") + + else: + self.jamf_management_btn.configure(state="disabled") + + def query_jamf_id(self): + """ + query jamf for specific computer record + """ + self.reset_data() + + try: + + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("ID Mode selected.") + + # + # request specific jamf computer record + url = self.jamf_hostname+ '/JSSResource/computers/id/' + self.id_string.get() + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + if response.code != 200: + self.status_string.set("%i returned." % response.code) + return + + # + # begin populating display strings + self.computer_name_string.set(response_json['computer']['general']['name']) + self.assettag_string.set(response_json['computer']['general']['asset_tag']) + self.barcode_string.set(response_json['computer']['general']['barcode_1']) + self.username_string.set(response_json['computer']['location']['username']) + + self.email_string.set(response_json['computer']['location']['email_address']) + self.fullname_string.set(response_json['computer']['location']['real_name']) + self.phone_string.set(response_json['computer']['location']['phone']) + self.room_string.set(response_json['computer']['location']['room']) + self.building_string.set(response_json['computer']['location']['building']) + self.position_string.set(response_json['computer']['location']['position']) + self.department_string.set(response_json['computer']['location']['department']) + self.platform = response_json['computer']['general']['platform'] + + self.jamf_management = response_json['computer']['general']['remote_management']['managed'] + + if self.jamf_management is True: + self.jamf_management_btn.configure(text="True") + elif self.jamf_management is False: + self.jamf_management_btn.configure(text="False") + else: + print("else Jamf managment: %r" % self.jamf_management) + + # + # if you desire to add EA's you will need to find each by parsing all of the EA's + # + + # jss_purpose_raw = response_json['computer']['extension_attributes'] + # for ea in jss_purpose_raw: + # if ea['name'] == 'EA1': + # self.ea1_string.set(ea['value']) + # elif ea['name'] == 'EA2': + # self.ea2_string.set(ea['value']) + # elif ea['name'] == 'EA3': + # self.ea3_string.set(ea['value']) + # + # etc + # + + # + # handle communication errors + except urllib2.HTTPError, error: + if error.code == 400: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Request error.")) + elif error.code == 401: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Authorization error.")) + elif error.code == 403: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Permissions error.")) + elif error.code == 404: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource not found.")) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0])) + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Generic error.")) + + return + except urllib2.URLError, error: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error contacting Jamf.") + return + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying Jamf. [%s]" % exception_message) + return + + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Jamf returned info for ID %s." % self.id_string.get()) + self.button_state() + + def query_jamf_me(self): + """ + Query jamf about this particular machine + """ + + # + # this method finds the UUID for the local machine + # query's jamf and parses the ID from the record + # and then calls the main query method + # it's wasteful the first time it's called. + if not self.local_jamf_id: + + if platform.system() == 'Darwin': + local_uuid_raw = subprocess.check_output(["system_profiler", "SPHardwareDataType"]) + local_uuid = re.findall(r'Hardware UUID: (.*)', local_uuid_raw)[0] + elif platform.system() == 'Windows': + local_uuid_raw = subprocess.check_output("wmic CsProduct Get UUID") + local_uuid_raw = local_uuid_raw.split("\r\r\n")[1] + local_uuid = local_uuid_raw.split(" ")[0] + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Missing native UUID discovery.") + return + + # + # communicate with Jamf server + try: + url = self.jamf_hostname + '/JSSResource/computers/udid/' + local_uuid + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + self.local_jamf_id = response_json['computer']['general']['id'] + self.id_string.set(self.local_jamf_id) + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Request error.")) + elif error.code == 401: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Authorization error.")) + elif error.code == 403: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Permissions error.")) + elif error.code == 404: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource not found.")) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0])) + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Generic error.")) + + return + except urllib2.URLError, error: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error contacting JAMF server.") + return + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying Jamf. [%s]" % exception_message) + return + + else: + self.id_string.set(self.local_jamf_id) + + self.query_jamf_id() + + def populate_menu(self, menu_choice): + """ + builds list from static data source in jamf + """ + + # + # this method builds lists that can then be used to build combobox or popup menus from + # departments, buildings, sites + url = self.jamf_hostname + '/JSSResource/' + menu_choice + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + if response.code != 200: + return + + menu_items = ['None'] + for item in response_json[menu_choice]: + menu_items.append(item.get('name')) + return menu_items + + def populate_ea_menu(self, ea_id): + """ + builds list from extension attribute in jamf + """ + + # + # this method builds lists that can then be used to build combobox or popup menus from EA's + url = self.jamf_hostname + '/JSSResource/computerextensionattributes/id/' + str(ea_id) + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + response_json = json.loads(response.read()) + + if response.code != 200: + return + + choices = response_json['computer_extension_attribute']['input_type']['popup_choices'] + choices = ['None'] + choices + return choices + + def search_string_jamf(self): + """ + This method handles searching Jamf with a string + """ + def double_click(*event): + """ + handle clicks + """ + + # + # when a click occurs, parse out ID from string and call query method + selected = listbox.get(listbox.curselection()) + trim_select = re.search(r'\((.*)\)', selected).group(1) + + self.id_string.set(trim_select) + self.query_jamf_id() + + self.root.lift() + + if self.search_string.get() == "" or self.search_string.get().replace(" ", "") == "": + if self.id_string.get(): + self.query_jamf_id() + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("Searched for ID.") + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("No search string entered.") + + else: + # + # erase previous displayed values +# self.reset_data() + + # + # encode special characters included in search string + url = self.search_string.get() + url = urllib.quote(url, ':/()') + url = self.jamf_hostname + '/JSSResource/computers/match/*' + url + '*' + url = urllib.quote(url, ':/()') + + # + # communicate with Jamf server + try: + request = urllib2.Request(url) + request.add_header('Authorization', 'Basic ' + base64.b64encode(self.jamf_username + ':' + self.jamf_password)) + + response = urllib2.urlopen(request) + + # + # a non-200 response is bad, report and return + if response.code != 200: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("%i returned." % response.code) + return + + # + # handle various communication errors + except urllib2.HTTPError, error: + if error.code == 400: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Request error.")) + elif error.code == 401: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Authorization error.")) + elif error.code == 403: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Permissions error.")) + elif error.code == 404: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource not found.")) + elif error.code == 409: + contents = error.read() + error_message = re.findall(r"Error: (.*)<", contents) + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Resource conflict. " + error_message[0])) + else: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("HTTP code %i: %s " % (error.code, "Generic error.")) + + return + except urllib2.URLError, error: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error contacting JAMF server.") + return + except Exception as exception_message: + self.status_label.configure(style='Warning.TLabel') + self.status_string.set("Error querying Jamf. [%s]" % exception_message) + return + + # + # begin parsing data returned from Jamf + jamf_dom = parseString(response.read()) + self.status_label.configure(style='Normal.TLabel') + self.status_string.set("%i matches returned." % int(jamf_dom.getElementsByTagName('size')[0].childNodes[0].nodeValue)) + + match_results = [] + + # + # parse each returned computer element, retaining Jamf ID and Computer name + # properly format value to display + # build a version of the computer name used to sort by + # these rules are for our environment and may have no effect in yours. + # if the name is in the format "labmac-1" the integer is expanded to -0001 + # if the name is in the format "[lost] labmac-1" the sorting name is stored as + # "labmac-1a" to differentiate it from "labmac-1" + # the values are added to a list containing the previously processed values as + # [sorting name, computer name, jamf id] + + for node in jamf_dom.getElementsByTagName('computer'): + match_id = int(node.getElementsByTagName('id')[0].childNodes[0].nodeValue) + try: + match_name = node.getElementsByTagName('name')[0].childNodes[0].nodeValue + except: + match_name = "Not named." + + name_trim = match_name + + try: + number_part = re.search(r'(\d+)', name_trim).group(1) + number_free = "".join([i for i in name_trim if not i.isdigit()]) + expanded_number = '{:04d}'.format(int(number_part)) + expanded_x = number_free + expanded_number + name_trim = expanded_x + except: + pass + + if "[" in name_trim: + name_trim = re.search(r']([ -]*)(.*)', name_trim).group(2) + name_trim = str(name_trim) + "a" + + match_results.append([name_trim, match_name, match_id]) + + + # + # if there were returned results, build and display search results window + # + # + # position results window next to the main window, even if it has moved + # from the original location + # while sorting the list based on the synthetic string, + # display the computer name and ID + # bind clicks to function + if match_results: + + search_window = Toplevel() + + split_geom = self.root.winfo_geometry().split("+") + r_h = int(split_geom[0].split("x")[0]) + r_pos_x = int(split_geom[1]) + r_pos_y = int(split_geom[2]) + + search_window_geo = "%ix%i+%i+%i" % (190, 400, (r_h + r_pos_x + 10), (r_pos_y)) + search_window.geometry(search_window_geo) + + search_window.title("Search results") + + list_frame = ttk.Frame(search_window, width=190, height=400, padding=(4, 0, 0, 0)) + + scrollbar = Scrollbar(list_frame) + scrollbar.pack(side=RIGHT, fill=Y) + + listbox = Listbox(list_frame, bd=0, yscrollcommand=scrollbar.set, selectmode=SINGLE, width=190, height=400) + listbox.pack() + + scrollbar.config(command=listbox.yview) + + list_frame.pack() + + for item in sorted(match_results): + insert_string = item[1] +" (" + str(item[2]) + ")" + listbox.insert(END, insert_string) + + listbox.bind("<>", double_click) + + search_window.mainloop() + + +def login(): + """ + if the user has proper privleges, consider them an authorized user and proceed + """ + def try_login(): + """ + jamf api call for login test + """ + try: + url = jamf_hostname.get() + '/JSSResource/accounts/username/' + jamf_username.get() + request = urllib2.Request(url) + request.add_header('Accept', 'application/json') + request.add_header('Authorization', 'Basic ' + base64.b64encode(jamf_username.get() + ':' + jamf_password.get())) + + response = urllib2.urlopen(request) + + if response.code != 200: + tkMessageBox.showerror("Jamf login", "Invalid response from Jamf") + root.destroy() # clean up after yourself! + sys.exit() + + response_json = json.loads(response.read()) + + # + # store list of user privileges + user_privileges = response_json['account']['privileges']['jss_objects'] + + # + # stop number of require privileges + count_privileges = len(required_privileges) + + # + # for every required privilege + # check if it's in user privileges + # decrement if yes + for item in required_privileges: + if item in user_privileges: + count_privileges -= 1 + + # + # if all require privileges accounted for, proceed + # else alert and fail + if count_privileges == 0: + root.destroy() # clean up after yourself! + return + else: + tkMessageBox.showerror("Jamf login", "User lacks appropriate privileges.") + + except: + tkMessageBox.showerror("Jamf login", "Invalid username or password.") + sys.exit() + + sys.exit() + + # + # This is really important. This list contains the required rights. + required_privileges = ['Read Accounts', 'Read Buildings', 'Read Computers', 'Update Computers', 'Read Departments', 'Read User', 'Update User'] + + root = Tk() + jamf_username = StringVar() + jamf_password = StringVar() + jamf_hostname = StringVar() + + # customizable for specific deployment + jamf_hostname.set("https://your.jamf.server:8443") + + # + # build and display login screen + root.title("Jamf Login") + mainframe = ttk.Frame(root) + mainframe.grid(column=0, row=0, sticky=NSEW) + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + root.geometry('+0+0') + + ttk.Label(mainframe, text="Jamf Server:").grid(column=1, row=10, sticky=E) + uname_entry = ttk.Entry(mainframe, width=30, textvariable=jamf_hostname) + uname_entry.grid(column=2, row=10, sticky=EW) + + ttk.Label(mainframe, text="Username:").grid(column=1, row=20, sticky=E) + uname_entry = ttk.Entry(mainframe, width=30, textvariable=jamf_username) + uname_entry.grid(column=2, row=20, sticky=EW) + + ttk.Label(mainframe, text="Password:").grid(column=1, row=30, sticky=E) + pword_entry = ttk.Entry(mainframe, width=30, textvariable=jamf_password, show="*") + pword_entry.grid(column=2, row=30, sticky=EW) + + ttk.Button(mainframe, text="Quit", command=sys.exit).grid(column=2, row=70, padx=3) + ttk.Button(mainframe, text="Login", default='active', command=try_login).grid(column=2, row=70, padx=3, sticky=E) + + if platform.system() == 'Darwin': + tmpl = 'tell application "System Events" to set frontmost of every process whose unix id is {} to true' + script = tmpl.format(os.getpid()) + output = subprocess.check_call(['/usr/bin/osascript', '-e', script]) + + root.bind('', lambda event: try_login()) + + uname_entry.focus() + root.mainloop() + + return (jamf_hostname.get(), jamf_username.get(), jamf_password.get()) + +def main(): + + jamf_hostname, jamf_username, jamf_password = login() + if not jamf_username: + sys.exit(0) + + if platform.system() == 'Darwin': + running_under_osx = True + local_uuid_raw = subprocess.check_output(["system_profiler", "SPHardwareDataType"]) + local_uuid = re.findall(r'Hardware UUID: (.*)', local_uuid_raw)[0] + else: + running_under_osx = False + local_uuid_raw = subprocess.check_output("wmic CsProduct Get UUID") + local_uuid_raw = local_uuid_raw.split("\r\r\n")[1] + local_uuid = local_uuid_raw.split(" ")[0] + + root = Tk() + my_app = Computer(root, local_uuid, running_under_osx, jamf_hostname, jamf_username, jamf_password) + + root.mainloop() + +if __name__ == '__main__': + main() diff --git a/tugboat/tugboat_icon.icns b/tugboat/tugboat_icon.icns new file mode 100644 index 0000000..7b0a53a Binary files /dev/null and b/tugboat/tugboat_icon.icns differ diff --git a/tugboat/tugboat_icon.ico b/tugboat/tugboat_icon.ico new file mode 100644 index 0000000..4ac1c30 Binary files /dev/null and b/tugboat/tugboat_icon.ico differ diff --git a/tugboat/tugboat_pc.zip b/tugboat/tugboat_pc.zip new file mode 100644 index 0000000..848191b Binary files /dev/null and b/tugboat/tugboat_pc.zip differ