コード例 #1
0
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)
コード例 #2
0
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
コード例 #3
0
ファイル: jamf_api_tool.py プロジェクト: zackn9ne/jamf-upload
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)
コード例 #4
0
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
コード例 #5
0
ファイル: jamf_api_tool.py プロジェクト: zackn9ne/jamf-upload
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()
コード例 #6
0
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
コード例 #7
0
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)
コード例 #8
0
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)
コード例 #9
0
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)
コード例 #10
0
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.")
コード例 #11
0
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