def upload_computergroup( jamf_url, enc_creds, computergroup_name, template_contents, cli_custom_keys, verbosity, obj_id=None, ): """Upload computer group""" # if we find an object ID we put, if not, we post if obj_id: url = "{}/JSSResource/computergroups/id/{}".format(jamf_url, obj_id) else: url = "{}/JSSResource/computergroups/id/0".format(jamf_url) if verbosity > 2: print("Computer Group data:") print(template_contents) print("Uploading Computer Group...") # write the template to temp file template_xml = curl.write_temp_file(template_contents) count = 0 while True: count += 1 if verbosity > 1: print("Computer Group upload attempt {}".format(count)) method = "PUT" if obj_id else "POST" r = curl.request(method, url, enc_creds, verbosity, template_xml) # check HTTP response if curl.status_check(r, "Computer Group", computergroup_name) == "break": break if count > 5: print( "WARNING: Computer Group upload did not succeed after 5 attempts" ) print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(30) if verbosity > 1: api_get.get_headers(r) # clean up temp files if os.path.exists(template_xml): os.remove(template_xml)
def check_pkg(pkg_name, jamf_url, enc_creds, verbosity): """check if a package with the same name exists in the repo note that it is possible to have more than one with the same name which could mess things up""" url = "{}/JSSResource/packages/name/{}".format(jamf_url, quote(pkg_name)) r = curl.request("GET", url, enc_creds, verbosity) if r.status_code == 200: obj = json.loads(r.output) try: obj_id = str(obj["package"]["id"]) except KeyError: obj_id = "-1" else: obj_id = "-1" return obj_id
def delete(id, jamf_url, enc_creds, verbosity): """deletes a policy by obtained or set id""" url = "{}/JSSResource/policies/id/{}".format(jamf_url, id) count = 0 while True: count += 1 if verbosity > 1: print("Policy delete attempt {}".format(count)) request_type = "DELETE" r = curl.request(request_type, url, enc_creds, verbosity) # check HTTP response if curl.status_check(r, "Policy", id, request_type) == "break": break if count > 5: print("WARNING: Policy delete did not succeed after 5 attempts") print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(30) if verbosity > 1: api_get.get_headers(r)
def curl_pkg(pkg_name, pkg_path, jamf_url, enc_creds, obj_id, r_timeout, verbosity): """uploads the package using curl""" url = "{}/dbfileupload".format(jamf_url) additional_headers = [ "--header", "DESTINATION: 0", "--header", "OBJECT_ID: {}".format(obj_id), "--header", "FILE_TYPE: 0", "--header", "FILE_NAME: {}".format(pkg_name), "--connect-timeout", str("60"), "--max-time", str(r_timeout), ] r = curl.request("POST", url, enc_creds, verbosity, pkg_path, additional_headers) if verbosity: print("HTTP response: {}".format(r.status_code)) return r.output
def main(): """Do the main thing here""" print("\n** Jamf API Tool for Jamf Pro.\n") # parse the command line arguments args = get_args() verbosity = args.verbose # grab values from a prefs file if supplied jamf_url, _, _, slack_webhook, enc_creds = api_connect.get_creds_from_args(args) if args.slack: if not slack_webhook: print("slack_webhook value error. Please set it in your prefs file.") exit() # computers block #### if args.computer: if args.search and args.all: exit("syntax error: use either --search or --all, but not both") if not args.all: exit("syntax error: --computers requires --all as a minimum") recent_computers = [] # we'll need this later old_computers = [] warning = [] # stores full detailed computer info compliant = [] if args.all: """ fill up computers []""" obj = api_get.check_api_finds_all( jamf_url, "computer", enc_creds, verbosity ) try: computers = [] for x in obj: computers.append(x["id"]) except IndexError: computers = "404 computers not found" print(f"{len(computers)} computers found on {jamf_url}") for x in computers: """ load full computer info now """ print(f"...loading info for computer {x}") obj = api_get.get_api_obj_value_from_id( jamf_url, "computer", x, "", enc_creds, verbosity ) if obj: """ this is now computer object """ try: macos = obj["hardware"]["os_version"] name = obj["general"]["name"] dep = obj["general"]["management_status"]["enrolled_via_dep"] seen = datetime.strptime( obj["general"]["last_contact_time"], "%Y-%m-%d %H:%M:%S" ) now = datetime.utcnow() except IndexError: macos = "unknown" name = "unknown" dep = "unknown" seen = "unknown" now = "unknown" difference = (now - seen).days try: if (now - seen).days < 10 and not args.os: # if recent recent_computers.append( f"{x} {macos}\t" + f"name : {name}\n" + f"\t\tDEP : {dep}\n" + f"\t\tseen : {difference} days ago" ) if (now - seen).days < 10 and args.os and (macos >= args.os): compliant.append( f"{x} {macos}\t" + f"name : {name}\n" + f"\t\tDEP : {dep}\n" + f"\t\tseen : {difference} days ago" ) elif (now - seen).days < 10 and args.os and (macos < args.os): warning.append( f"{x} {macos}\t" + f"name : {name}\n" + f"\t\tDEP : {dep}\n" + f"\t\tseen : {difference} days ago" ) if (now - seen).days > 10: old_computers.append( f"{x} {macos}\t" + f"name : {name}\n" + f"\t\tDEP : {dep}\n" + f"\t\tseen : {difference} days ago" ) except IndexError: print("checkin calc. error") # recent_computers.remove(f"{macos} {name} dep:{dep} seen:{calc}") # query is done print(bcolors.OKCYAN + "Loading complete...\n\nSummary:" + bcolors.ENDC) if args.os: """ summarise os """ if compliant: print(f"{len(compliant)} compliant and recent:") for x in compliant: print(bcolors.OKGREEN + x + bcolors.ENDC) if warning: print(f"{len(warning)} non-compliant:") for x in warning: print(bcolors.WARNING + x + bcolors.ENDC) if old_computers: print(f"{len(old_computers)} stale - OS version not considered:") for x in old_computers: print(bcolors.FAIL + x + bcolors.ENDC) else: """ regular summary """ print(f"{len(recent_computers)} last check-in within the past 10 days") for x in recent_computers: print(bcolors.OKGREEN + x + bcolors.ENDC) print(f"{len(old_computers)} stale - last check-in more than 10 days") for x in old_computers: print(bcolors.FAIL + x + bcolors.ENDC) if args.slack: # end a slack api webhook with this number score = len(recent_computers) / (len(old_computers) + len(recent_computers)) score = "{:.2%}".format(score) slack_payload = str( f":hospital: update health: {score} - {len(old_computers)} " f"need to be fixed on {jamf_url}\n" ) print(slack_payload) data = {"text": slack_payload} for x in old_computers: print(bcolors.WARNING + x + bcolors.ENDC) if args.slack: # werk slack_payload += str(f"{x}\n") if args.slack: # send to slack data = {"text": slack_payload} url = slack_webhook request_type = "POST" curl.request(request_type, url, enc_creds, verbosity, data) exit() # policy block ##### if args.policies: if args.search and args.all: exit("syntax error: use --search or --all not both") if args.search: query = args.search obj = api_get.check_api_finds_all(jamf_url, "policy", enc_creds, verbosity) if obj: # targets is the new list targets = [] print( f"Searching {len(obj)} policy/ies on {jamf_url}:\n" "To delete policies, obtain a matching query, then run with the " "--delete argument" ) for x in query: for obj_item in obj: # do the actual search if x in obj_item["name"]: targets.append(obj_item.copy()) if len(targets) > 0: print("Policies found:") for target in targets: print( bcolors.WARNING + f"- policy {target['id']}" + f"\tname : {target['name']}" + bcolors.ENDC ) if args.delete: delete(target["id"], jamf_url, enc_creds, verbosity) print("{} total matches".format(len(targets))) else: for partial in query: print("No match found: {}".format(partial)) elif args.all: # assumes --policy flag due to prior exit on another style obj = api_get.check_api_finds_all( jamf_url, "category_all", enc_creds, verbosity ) if obj: for x in obj: # loop all the categories print( bcolors.OKCYAN + f"category {x['id']}\t{x['name']}" + bcolors.ENDC ) obj = api_get.get_policies_in_category( jamf_url, "category_all_items", x["id"], enc_creds, verbosity ) if obj: for x in obj: # loop all the policies # gather interesting info for each policy via API # use a single call # general/name # scope/computer_groups [0]['name'] generic_info = api_get.get_api_obj_value_from_id( jamf_url, "policy", x["id"], "", enc_creds, verbosity ) name = generic_info["general"]["name"] try: groups = generic_info["scope"]["computer_groups"][0][ "name" ] except IndexError: groups = "none" try: pkg = generic_info["package_configuration"]["packages"][ 0 ]["name"] except IndexError: pkg = "none" # now show all the policies as each category loops print( bcolors.WARNING + f" policy {x['id']}" + f"\tname : {x['name']}\n" + bcolors.ENDC + f"\t\tpkg : {pkg}\n" + f"\t\tscope : {groups}" ) else: print("something went wrong: no categories found.") print( "\n" + bcolors.OKGREEN + "All policies listed above.. program complete for {}".format(jamf_url) + bcolors.ENDC ) exit else: exit("syntax error: with --policies use --search or --all.") # set a list of names either from the CLI for Category erase all if args.category: categories = args.category print( "categories to check are:\n{}\nTotal: {}".format( categories, len(categories) ) ) # now process the list of categories for category_name in categories: category_name = category_name.replace(" ", "%20") # return all items found in each category print("\nChecking '{}' on {}".format(category_name, jamf_url)) obj = api_get.get_policies_in_category( jamf_url, "category_all_items", category_name, enc_creds, verbosity ) if obj: if not args.delete: print( f"Category '{category_name}' exists with {len(obj)} items: " "To delete them run this command again with the --delete flag." ) for obj_item in obj: print("~^~ {} -~- {}".format(obj_item["id"], obj_item["name"])) if args.delete: delete(obj_item["id"], jamf_url, enc_creds, verbosity) else: print("Category '{}' not found".format(category_name)) # process a name or list of names if args.names: names = args.names print("policy names to check are:\n{}\nTotal: {}".format(names, len(names))) for policy_name in names: print("\nChecking '{}' on {}".format(policy_name, jamf_url)) obj_id = api_get.get_api_obj_id_from_name( jamf_url, "policy", policy_name, enc_creds, verbosity ) if obj_id: # gather info from interesting parts of the policy API # use a single call # general/name # scope/computer_gropus [0]['name'] generic_info = api_get.get_api_obj_value_from_id( jamf_url, "policy", obj_id, "", enc_creds, verbosity ) name = generic_info["general"]["name"] try: groups = generic_info["scope"]["computer_groups"][0]["name"] except IndexError: groups = "" print("Match found: '{}' ID: {} Group: {}".format(name, obj_id, groups)) if args.delete: delete(obj_id, jamf_url, enc_creds, verbosity) else: print("Policy '{}' not found".format(policy_name)) print()
def upload_mobileconfig( jamf_url, enc_creds, mobileconfig_name, description, category, mobileconfig_plist, computergroup_name, template_contents, profile_uuid, verbosity, obj_id=None, ): """Update Configuration Profile metadata.""" # if we find an object ID we put, if not, we post if obj_id: url = "{}/JSSResource/osxconfigurationprofiles/id/{}".format(jamf_url, obj_id) else: url = "{}/JSSResource/osxconfigurationprofiles/id/0".format(jamf_url) # remove newlines, tabs, leading spaces, and XML-escape the payload mobileconfig_plist = mobileconfig_plist.decode("UTF-8") mobileconfig_list = mobileconfig_plist.rsplit("\n") mobileconfig_list = [x.strip("\t") for x in mobileconfig_list] mobileconfig_list = [x.strip(" ") for x in mobileconfig_list] mobileconfig = "".join(mobileconfig_list) # substitute user-assignable keys replaceable_keys = { "mobileconfig_name": mobileconfig_name, "description": description, "category": category, "payload": mobileconfig, "computergroup_name": computergroup_name, "uuid": "com.github.grahampugh.jamf-upload.{}".format(profile_uuid), } # substitute user-assignable keys (escaping for XML) template_contents = actions.substitute_assignable_keys( template_contents, replaceable_keys, verbosity, xml_escape=True ) if verbosity > 2: print("Configuration Profile to be uploaded:") print(template_contents) print("Uploading Configuration Profile..") # write the template to temp file template_xml = curl.write_temp_file(template_contents) count = 0 while True: count += 1 if verbosity > 1: print("Configuration Profile upload attempt {}".format(count)) method = "PUT" if obj_id else "POST" r = curl.request(method, url, enc_creds, verbosity, template_xml) # check HTTP response if curl.status_check(r, "Configuration Profile", mobileconfig_name) == "break": break if count > 5: print( "ERROR: Configuration Profile upload did not succeed after 5 attempts" ) print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(10) if verbosity > 1: api_get.get_headers(r) return r
def upload_script( jamf_url, script_name, script_path, category_id, category_name, script_info, script_notes, script_priority, script_parameter4, script_parameter5, script_parameter6, script_parameter7, script_parameter8, script_parameter9, script_parameter10, script_parameter11, script_os_requirements, verbosity, token, cli_custom_keys, obj_id=None, ): """Update script metadata.""" # import script from file and replace any keys in the script # script_contents = Path(script_path).read_text() with open(script_path, "r") as file: script_contents = file.read() # substitute user-assignable keys # pylint is incorrectly stating that 'verbosity' has no value. So... # pylint: disable=no-value-for-parameter script_contents = actions.substitute_assignable_keys( script_contents, cli_custom_keys, verbosity) # priority has to be in upper case. Let's make it nice for the user if script_priority: script_priority = script_priority.upper() # build the object script_data = { "name": script_name, "info": script_info, "notes": script_notes, "priority": script_priority, "categoryId": category_id, "categoryName": category_name, "parameter4": script_parameter4, "parameter5": script_parameter5, "parameter6": script_parameter6, "parameter7": script_parameter7, "parameter8": script_parameter8, "parameter9": script_parameter9, "parameter10": script_parameter10, "parameter11": script_parameter11, "osRequirements": script_os_requirements, "scriptContents": script_contents, } # ideally we upload to the object ID but if we didn't get a good response # we fall back to the name if obj_id: url = "{}/uapi/v1/scripts/{}".format(jamf_url, obj_id) script_data["id"] = obj_id else: url = "{}/uapi/v1/scripts".format(jamf_url) if verbosity > 2: print("Script data:") print(script_data) print("Uploading script..") count = 0 script_json = curl.write_json_file(script_data) while True: count += 1 if verbosity > 1: print("Script upload attempt {}".format(count)) method = "PUT" if obj_id else "POST" r = curl.request(method, url, token, verbosity, script_json) # check HTTP response if curl.status_check(r, "Script", script_name) == "break": break if count > 5: print("ERROR: Script upload did not succeed after 5 attempts") print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(10) if verbosity > 1: api_get.get_headers(r) # clean up temp files if os.path.exists(script_json): os.remove(script_json)
def update_pkg_metadata(jamf_url, enc_creds, pkg_name, pkg_metadata, hash_value, verbosity, pkg_id=None): """Update package metadata. Currently only serves category""" if hash_value: hash_type = "SHA_512" else: hash_type = "MD5" # build the package record XML pkg_data = ( "<package>" + f"<name>{pkg_name}</name>" + f"<filename>{pkg_name}</filename>" + f"<category>{escape(pkg_metadata['category'])}</category>" + f"<info>{escape(pkg_metadata['info'])}</info>" + f"<notes>{escape(pkg_metadata['notes'])}</notes>" + f"<priority>{pkg_metadata['priority']}</priority>" + f"<reboot_required>{pkg_metadata['reboot_required']}</reboot_required>" + f"<required_processor>{pkg_metadata['required_processor']}</required_processor>" + f"<os_requirement>{pkg_metadata['os_requirement']}</os_requirement>" + f"<hash_type>{hash_type}</hash_type>" + f"<hash_value>{hash_value}</hash_value>" + f"<send_notification>{pkg_metadata['send_notification']}</send_notification>" + "</package>") # ideally we upload to the package ID but if we didn't get a good response # we fall back to the package name if pkg_id: method = "PUT" url = "{}/JSSResource/packages/id/{}".format(jamf_url, pkg_id) else: method = "POST" url = "{}/JSSResource/packages/name/{}".format(jamf_url, pkg_name) if verbosity > 2: print("Package data:") print(pkg_data) count = 0 while True: count += 1 if verbosity > 1: print(f"Package metadata upload attempt {count}") pkg_xml = curl.write_temp_file(pkg_data) r = curl.request(method, url, enc_creds, verbosity, pkg_xml) # check HTTP response if curl.status_check(r, "Package", pkg_name) == "break": break if count > 5: print( "WARNING: Package metadata update did not succeed after 5 attempts" ) print(f"HTTP POST Response Code: {r.status_code}") print("ERROR: Package metadata upload failed ") exit(-1) sleep(30) if verbosity: api_get.get_headers(r) # clean up temp files if os.path.exists(pkg_xml): os.remove(pkg_xml)
def upload_ea( jamf_url, enc_creds, ea_name, script_path, verbosity, cli_custom_keys, obj_id=None, ): """Update extension attribute metadata.""" # import script from file and replace any keys in the script with open(script_path, "r") as file: script_contents = file.read() # substitute user-assignable keys # pylint is incorrectly stating that 'verbosity' has no value. So... # pylint: disable=no-value-for-parameter script_contents = actions.substitute_assignable_keys( script_contents, cli_custom_keys, verbosity ) # XML-escape the script script_contents_escaped = escape(script_contents) # build the object ea_data = ( "<computer_extension_attribute>" + "<name>{}</name>".format(ea_name) + "<enabled>true</enabled>" + "<description/>" + "<data_type>String</data_type>" + "<input_type>" + " <type>script</type>" + " <platform>Mac</platform>" + " <script>{}</script>".format(script_contents_escaped) + "</input_type>" + "<inventory_display>Extension Attributes</inventory_display>" + "<recon_display>Extension Attributes</recon_display>" + "</computer_extension_attribute>" ) # if we find an object ID we put, if not, we post if obj_id: url = "{}/JSSResource/computerextensionattributes/id/{}".format( jamf_url, obj_id ) else: url = "{}/JSSResource/computerextensionattributes/id/0".format(jamf_url) if verbosity > 2: print("Extension Attribute data:") print(ea_data) print("Uploading Extension Attribute..") # write the template to temp file template_xml = curl.write_temp_file(ea_data) count = 0 while True: count += 1 if verbosity > 1: print("Extension Attribute upload attempt {}".format(count)) method = "PUT" if obj_id else "POST" r = curl.request(method, url, enc_creds, verbosity, template_xml) # check HTTP response if curl.status_check(r, "Extension Attribute", ea_name) == "break": break if count > 5: print("ERROR: Extension Attribute upload did not succeed after 5 attempts") print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(10) if verbosity > 1: api_get.get_headers(r) # clean up temp files if os.path.exists(template_xml): os.remove(template_xml)
def upload_policy_icon( jamf_url, enc_creds, policy_name, policy_icon_path, replace_icon, verbosity, obj_id=None, ): """Upload an icon to the policy that was just created""" # check that the policy exists. # Use the obj_id if we have it, or use name if we don't have it yet # We may need a wait loop here for new policies if not obj_id: # check for existing policy print("\nChecking '{}' on {}".format(policy_name, jamf_url)) obj_id = api_get.get_api_obj_id_from_name(jamf_url, "policy", policy_name, enc_creds, verbosity) if not obj_id: print( "ERROR: could not locate ID for policy '{}' so cannot upload icon" .format(policy_name)) return # Now grab the name of the existing icon using the API existing_icon = api_get.get_api_obj_value_from_id( jamf_url, "policy", obj_id, "self_service/self_service_icon/filename", enc_creds, verbosity, ) # If the icon naame matches that we already have, don't upload again # unless --replace-icon is set policy_icon_name = os.path.basename(policy_icon_path) if existing_icon != policy_icon_name or replace_icon: url = "{}/JSSResource/fileuploads/policies/id/{}".format( jamf_url, obj_id) print("Uploading icon...") count = 0 while True: count += 1 if verbosity > 1: print("Icon upload attempt {}".format(count)) r = curl.request("POST", url, enc_creds, verbosity, policy_icon_path) # check HTTP response if curl.status_check(r, "Icon", policy_icon_name) == "break": break if count > 5: print("WARNING: Icon upload did not succeed after 5 attempts") print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(30) if verbosity > 1: api_get.get_headers(r) else: print("Existing icon matches local resource - skipping upload.")
def upload_category(jamf_url, category_name, priority, verbosity, token, obj_id=0): """Update category metadata.""" # build the object category_data = {"priority": priority, "name": category_name} if obj_id: url = "{}/uapi/v1/categories/{}".format(jamf_url, obj_id) category_data["name"] = category_name else: url = "{}/uapi/v1/categories".format(jamf_url) if verbosity > 2: print("Category data:") print(category_data) print("Uploading category..") count = 0 # we cannot PUT a category of the same name due to a bug in Jamf Pro (PI-008157). # so we have to do a first pass with a temporary different name, then change it back... if obj_id: category_name_temp = category_name + "_TEMP" category_data_temp = {"priority": priority, "name": category_name_temp} category_json_temp = curl.write_json_file(category_data_temp) while True: count += 1 if verbosity > 1: print("Category upload attempt {}".format(count)) r = curl.request("PUT", url, token, verbosity, category_json_temp) # check HTTP response if curl.status_check(r, "Category", category_name_temp) == "break": break if count > 5: print( "ERROR: Temporary category update did not succeed after 5 attempts" ) print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(10) # write the category. If updating an existing category, this reverts the name to its original. category_json = curl.write_json_file(category_data) while True: count += 1 if verbosity > 1: print("Category upload attempt {}".format(count)) method = "PUT" if obj_id else "POST" r = curl.request(method, url, token, verbosity, category_json) # check HTTP response if curl.status_check(r, "Category", category_name) == "break": break if count > 5: print("ERROR: Category creation did not succeed after 5 attempts") print("\nHTTP POST Response Code: {}".format(r.status_code)) break sleep(10) if verbosity > 1: api_get.get_headers(r) # clean up temp files for file in category_json, category_json_temp: if os.path.exists(file): os.remove(file) return r