Skip to content
James Reynolds edited this page Feb 8, 2023 · 34 revisions

Introduction

jctl is a generic CRUD (create, read, update, delete) utility for Jamf Pro records. jctl also includes some functionality aimed at specific record types using -S but for now its main usage is for CRUD operations. No matter which operation you're performing, the basic syntax is as follows.

jctl <plural_record_type> (-cud) (options)

Without any options, jctl defaults to reading data. You can do one of the other options by specifying -c, -u, or -d. You must specify a record type to work with, and it must be in the plural form. To get the latest list of supported records use jctl -h (see below for the output at the time of this writing).

jctl is not limited by the following examples. Because jctl is generic, these are only some of the things it can do, basically things that we've done ourselves and written down so we don't have to figure it out again.

Getting Help

To display help use the help command with jctl you can enter the following arguments:

jctl --help or jctl -h

For example..

jctl --help
usage: jctl [-h] [-c [CREATE ...]] [-u UPDATE] [-d] [-S [SUB_COMMAND ...]] [-i [ID ...]]
            [-n [NAME ...]] [-r [REGEX ...]] [-s SEARCHPATH] [-l] [-p PATH] [-j]
            [--quiet-as-a-mouse] [-C CONFIG] [-v] [--use-the-force-luke]
            [--andele-andele]
            [record]

positional arguments:
  record                Valid Jamf Records are: advancedcomputersearches,
                        advancedmobiledevicesearches, advancedusersearches, buildings,
                        byoprofiles, categories, classes, computerconfigurations,
                        computerextensionattributes, computergroups, computerreports,
                        computers, departments, directorybindings,
                        diskencryptionconfigurations, distributionpoints, dockitems,
                        ebooks, ibeacons, jsonwebtokenconfigurations, ldapservers,
                        macapplications, managedpreferenceprofiles,
                        mobiledeviceapplications, mobiledevicecommands,
                        mobiledeviceconfigurationprofiles,
                        mobiledeviceenrollmentprofiles, mobiledeviceextensionattributes,
                        mobiledeviceinvitations, mobiledeviceprovisioningprofiles,
                        mobiledevices, netbootservers, networksegments,
                        osxconfigurationprofiles, packages, patchexternalsources,
                        patchinternalsources, patchpolicies, patchsoftwaretitles,
                        peripherals, peripheraltypes, policies, printers,
                        removablemacaddresses, scripts, sites, softwareupdateservers,
                        userextensionattributes, usergroups, users, vppaccounts,
                        vppassignments, vppinvitations, webhooks

optional arguments:
  -h, --help            show this help message and exit
  -c [CREATE ...], --create [CREATE ...]
                        Create jamf record (e.g. '-n <rec_name> [other]')
  -u UPDATE, --update UPDATE
                        Update jamf record (e.g. '-u general={} -u name=123')
  -d, --delete          Delete jamf record
  -S [SUB_COMMAND ...], --sub-command [SUB_COMMAND ...]
                        Execute subcommand for record
  -i [ID ...], --id [ID ...]
                        Search for id matches
  -n [NAME ...], --name [NAME ...]
                        Search for exact name match
  -r [REGEX ...], --regex [REGEX ...]
                        Search for regular expression matches
  -s SEARCHPATH, --searchpath SEARCHPATH
                        Search for a path (e.g. '-s general/id==152'
  -l, --long            List long format
  -p PATH, --path PATH  Print out path (e.g. '-p general -p serial_number')
  -j, --json            Print json (for pretty pipe to `prettier --parser json`)
  --quiet-as-a-mouse    Don't print anything
  -C CONFIG, --config CONFIG
                        path to config file
  -v, --version         print version and exit
  --use-the-force-luke  Don't ask to delete. DANGER! This can delete everything!
  --andele-andele       Don't pause 3 seconds when updating or deleting without
                        confirmation. DANGER! This can delete everything FAST!

For examples please see https://github.com/univ-of-utah-marriott-library-apple/jctl/wiki/jctl

The jctl Workflow

All of the following examples were created by running jctl <record_type>, finding a record, printing the long form of the record jctl <record_type> -n <name> -l, and then finding the specific data that we wanted to work with. This is kind of the intended workflow.

Why? Because we didn't want to write APIs to change all of this data. We wanted it to be completely generic so that you aren't limited by an API that depends on us writing and maintaining. Yes, this exposes all of the guts of Jamf Pro to you, but we think we've done it in a way that can be digested (as opposed to doing this with curl or something else).

Read

Reading is the default action so you do not need to specify any flags to read.

To print the names of all records of a specific type just specify the record type. The following command will print the names of all computer records.

jctl computers

Search by name or id

To print the name of just one record, you search by name (-n) or id (-i).

jctl computers -n mac_pro

jctl computers -i 14

Search name with regular expression

To filter records by regular expression use -r.

jctl computers -r "^mac\d"

Print all of the data

To print all of the data add -l (for long).

jctl computers -r "^mac\d" -l

Searching through the data

To search for a specific value in a record use -s.

jctl policies -s self_service/self_service_icon==None

Find all policies that are run at startup.

jctl policies -s general/trigger_startup==true

Search operators include ==, !=, and for regular expressions, there is =~ and !=~ (~= still works but is deprecated).

You can use multiple searches but they are all "and" searches. So the following will find policies that have "Prod" and "Once" in their name.

jctl policies -s general/name=~Prod -s general/name=~Once

Printing specific data

To print specific values use -p.

jctl policies -p self_service/self_service_icon

Print the policy name, category, and other trigger (as json).

jctl policies -p general/name -p general/category/name -p general/trigger_other -j

View all of the policy self-service descriptions.

jctl policies -p general/name -p self_service/self_service_description -j

Create

To create a record you have to specify a name. Some record types require more data, and we haven't solved that problem yet.

Create a blank policy.

 jctl policies -c "install zoom"

Update

Updating can be tricky. We haven't tested every scenario, but we have run into problems when a list of items doesn't exist. For example, if a policy doesn't have any packages configured, you can't just add a package. Here's a workflow.

This sets the package filename (category needs to be set to blank for "No category assigned" or else it errors).

jctl packages -n zoom.pkg -u filename=zoom.pkg -u category=''

Create a blank policy.

jctl policies -c "install zoom"

This adds the package to a policy that doesn't have packages set up yet.

jctl policies -r "install zoom" -u package_configuration/packages="{'package': {'name': 'zoom.pkg', 'action': 'Install'}}"

You can remove packages from all policies that match the regular expression "install zoom".

jctl policies -r "install zoom" -u package_configuration/packages="{}"

If a package is already set, it can be changed like the following.

jctl policies -r "install zoom" -u package_configuration/packages/package/name=zoom.pkg

Update a policy computer group.

jctl policies -p general/name -p scope -s scope/computer_groups==None -s scope/departments==None -s scope/all_computers==false -s scope/computers==None -u scope/computer_groups="{'computer_group': {'id': '872', 'name': 'Students'}}"

Change a policy self-service description.

jctl policies -n "Install Brave" -u self_service/self_service_description="Brave 901.15.99"

Delete

Warning, moments after this feature was added a Jamf Pro server had to be restored from backup (in excitement someone forgot to add the -C dev_server.plist option). If you don't filter, -d will delete all records of the specified type. Since the feature was added, a confirmation was also added. It can be bypassed with the intentionally long flag --use-the-force-luke and an additional warning can be bypassed by adding --quiet-as-a-mouse and a delay can be bypassed by adding --andele-andele. These flags are really only for scripts and so they're intentionally long and have no shortcuts. I hope your script doesn't have any bugs. You have been warned.

This will delete all computers after asking for confirmation.

jctl computers -d

Delete all computers that have not contacted the server since 2020.

jctl computers -d -s general/last_contact_time~=2020

Subcommands

To display the subcommands that can be executed with a command with jctl you can enter the following arguments:

jctl [RECORD] -S

For example..

jctl patchsoftwaretitles -S
patchsoftwaretitles valid subcommands are:
  patchpolicies
  packages
  set_package_for_version
  set_all_packages

Subcommands are specific to certain record types.

Print a script (named msupdate.sh).

 jctl scripts -n msupdate.sh -p script_contents

Print a script that looks good (named msupdate.sh).

 jctl scripts -n msupdate.sh -S script_contents

Show the policies, patch policies, and computer groups that a package is referenced by. NOTE: Because this command downloads a lot of data from the server, it takes a long time before anything is shown.

jctl packages -S usage

Print a csv file with computers on one axis, and all apps and their versions on the other axis.

jctl computers -S apps

Show the patch policies for each patch software title. It lists patch software titles, followed by policies.

 jctl patchsoftwaretitles -S patchpolicies

View PatchSoftwareTitles packages.

 jctl patchsoftwaretitles -S packages

Set the package for a version.

 jctl patchsoftwaretitles -S set_package_for_version

Match PatchSoftwareTitles definition versions with packages named that fit the regex ".*-.pkg".

 jctl patchsoftwaretitles -S set_all_packages

Set a version for PatchPoliciy.

 jctl patchpolicies -r Zoom -S set_version 5.6

Print policy data for a spreadsheet application.

jctl policies -S spreadsheet

Jumpstarting a Jamf Pro Server

This is how you can jumpstart a brand new Jamf server using jctl and pkgctl. I have a brand new Jamf server running on my machine.

I've set the serial number, created the first user, accepted the Patch Management agreement, and configured jctl to point to my new server. I have done nothing else to the server.

To get the following examples to work, I must name my packages a certain way, mainly, "name-version.pkg". Right now the names are based on the names generated by AutoPkg but this can be changed. The version must appear exactly the same as the patch software titles versions.

Create Zoom software title

First, let's create a patch software title for Zoom. Unfortunately, I need Zoom's code, 0F9, to do this. I got the code by creating the title with the web interface and then used jctl to print the record details (patchsoftwaretitles -r Zoom -p name_id). We plan on automating this eventually.

This creates a patch software title record for Zoom.

jctl patchsoftwaretitles -c 0F9

Because I'm modifying the server, it will ask for confirmation first.

Create Zoom package records

If you don't already have packages uploaded to your server, for purposes of this example, we can create some placeholder package records. They won't actually have packages.

I need to print the versions for Zoom and use those in my package names (for the naming convention).

% jctl patchsoftwaretitles -r Zoom -p versions/version
Zoom Client for Meetings
[{'package': None, 'software_version': '5.8.4 (2421)'},
 {'package': None, 'software_version': '5.8.3 (2240)'},
 {'package': None, 'software_version': '5.8.1 (1983)'},
...

I used -r, to search names by regular expression so that I wouldn't have to type out the full name for Zoom.

Parenthesis are in the Jamf data and can't be changed but python-jamf replaces the parenthesis so it's easier to manage.

jctl packages -c "Zoom-5.8.1 (1983).pkg"
jctl packages -c "Zoom-5.8.3 (2240).pkg"
jctl packages -c "Zoom-5.8.4 (2421).pkg"

Verify Everything

jctl packages

You should see 3 packages.

Zoom-5.8.1 (1983).pkg
Zoom-5.8.3 (2240).pkg
Zoom-5.8.4 (2421).pkg
Count: 3

And verify the Zoom patch software title exists.

jctl patchsoftwaretitles

You should see this.

Zoom Client for Meetings
Count: 1

Match definitions

Because packages are so central to managing computers, we created a tool specifically to work with packages called pkgctl. pkgctl -p will define all packages for all patch titles.

pkgctl -p

Here it says it found the 3 Zoom packages and it went ahead and defined them.

% pkgctl -p
Updating patch definitions...
Matched Zoom-5.8.4 (2421).pkg
Matched Zoom-5.8.3 (2240).pkg
Matched Zoom-5.8.1 (1983).pkg

View Zoom Definitions

When I look at Zoom now it shows the packages that are defined for each version.

% jctl patchsoftwaretitles -r Zoom -p versions/version
Zoom Client for Meetings
[{'package': {'id': '1', 'name': 'Zoom-5.8.4 (2421).pkg'},
  'software_version': '5.8.4 (2421)'},
 {'package': {'id': '2', 'name': 'Zoom-5.8.3 (2240).pkg'},
  'software_version': '5.8.3 (2240)'},
 {'package': {'id': '3', 'name': 'Zoom-5.8.1 (1983).pkg'},
  'software_version': '5.8.1 (1983)'},
...

Get Zoom Details

Now let's create a policy for Zoom. First, we need the id of the software title. Here I print out all the details with -l (for long, like ls -l) so I can get the id. In this case, it's 1. (Note, the source_id is also 1 but it refers to something else.)

% jctl patchsoftwaretitles -r Zoom -l
{'category': {'id': '-1', 'name': 'No category assigned'},
 'id': ‘1',
 'name': 'Zoom Client for Meetings',
 'name_id': '0F9',
 'notifications': {'email_notification': 'true', 'web_notification': 'true'},
 'source_id': '1',
 'versions': {'version': [{'package': {'id': '1', 'name': 'Zoom-5.8.4 (2421).pkg'},
  ...

If you wanted to optimize the workflow you could just use this command.

% jctl patchsoftwaretitles -r Zoom -p id
Zoom Client for Meetings
['1']

Create Patch Policies

Now I can create some patch policies. Patch policies require 3 arguments, the name, the id of the patch software title, and the version.

% jctl patchpolicies -c "Auto" 1 "5.8.3 (2240)"
Server: http://localhost:8080/JSSResource
Are you sure you want to create a PatchPolicy named "Auto" [y/n]? y

% jctl patchpolicies -c "SS" 1 "5.8.4 (2421)"
Server: http://localhost:8080/JSSResource
Are you sure you want to create a PatchPolicy named "SS" [y/n]? y

I've created 2 policies, one named Auto and one SS (for self service).

If you see the error "Conflict: Error: 5.8.4 (2421) is not a recognized version for the software title", make sure you have the right id for Zoom (see the above step).

Using with jq

The -j flag outputs json. This is extremely useful when used with (jq)[https://stedolan.github.io/jq/]. Here are some examples of using jctl with jq.

Print all of the computer information in readable json.

jctl computers -l -j | jq .

Print computer information based on an advanced computer search (I'm thinking of building this into jctl but it's possible now with jq).

jctl computers -i `jctl advancedcomputersearches -r SOME_REGEX -p computers/computer/id -j | jq '.[] | to_entries[] | .value | join(" ")'| sed -e 's/"//g'`

Path Quirkiness

Right now, there is a slight quirkiness with how paths work. You can see this by comparing the search path for jctl and the search path for jq.

Notice that jq requires you to indicate when there's an array, which happens after partitions and partition.

> jctl computers -r REGEX -l -j | jq ".[].hardware.storage.device.partitions[].partition[] | [.name, .size]"
[
  "Macintosh HD (Boot Partition)",
  "494384"
]
[
  "Data",
  "494384"
]

jctl just ignores arrays and throws all of the results into an array at the end. This is the output of jctl 1.1.19.

> jctl computers -r REGEX -p hardware/storage/device/partitions/partition/name -p hardware/storage/device/partitions/partition/size -j | jq .
[
  {
    "hardware/storage/device/partitions/partition/name": [
      [
        "Macintosh HD (Boot Partition)",
        "Data"
      ]
    ],
    "hardware/storage/device/partitions/partition/size": [
      [
        "494384",
        "494384"
      ]
    ]
  }
]

This is the output of jctl 1.1.20 (not released as of Jan 26, 2023).

> jctl computers -r REGEX -p hardware/storage/device/partitions/partition/name -p hardware/storage/device/partitions/partition/size -j | jq .
[
  {
    "hardware": {
      "storage": {
        "device": {
          "partitions": {
            "partition": {
              "name": [
                [
                  "Macintosh HD (Boot Partition)",
                  "Data"
                ]
              ],
              "size": [
                [
                  "494384",
                  "494384"
                ]
              ]
            }
          }
        }
      }
    }
  }
]

Ideally, this is what we want the output to look like, which is closer to how jq works.

> jctl computers -r REGEX -p hardware/storage/device/partitions[]/partition[]/name -p hardware/storage/device/partitions/partition/size -j | jq .
[
  {
    "hardware": {
      "storage": {
        "device": {
          "partitions": {
            [
              {
                "partition": [
                  {
                    "name": "Macintosh HD (Boot Partition)",
                    "size": "494384"
                  },
                  {
                    "name": "Data",
                    "size": "494384"
                  }
                ]
              }
            ]
          }
        }
      }
    }
  }
]

Either way, you should just use jq! It's so powerful at organizing the data.

Clone this wiki locally