Skip to content

JNUC2019 Lab Session Presenter

Chris Lasell edited this page Oct 31, 2019 · 4 revisions

Hands On, Real-time Classic API Using ruby-jss

(Home)

Lab Session Contents - Presenter Notes

Jumping into IRB

  • Open Terminal.app or your preferred terminal
  • Run irb
% irb
irb(main):001:0>

A little about irb

  • 'irb' stands for interactive ruby, a real-time ruby interpreter

  • You can type ruby code into it - just like you type bash code into a shell

  • Enter a line of ruby code, it runs and displays the 'return value' after a =>

Require ruby-jss

  • In irb, tell ruby that you want to use ruby-jss:
require 'ruby-jss'
  • Ruby is now aware of a module called JSS which contains all of the ruby-jss

  • While we're requiring, lets do this:

require 'pp'
  • 'Pretty Print' allows us to examine ruby objects in irb in a more readable format.

Connecting to the Classic API

JSS.api.connect server: 'tryitout.jamfcloud.com', user: 'jnuc2019', pw: :prompt
  • Enter anything you'd like for the password

  • Here's what we did:

    • JSS.api is the default connection to the API
    • We tell it to connect giving it the values needed: server, user and password
    • If you know the password, you can pass it directly, in quotes, Just beware of security issues
    • If the password is correct, the connection is made, and the connect method returns the server name

Next Page

  • Now that we're connected, we'll do some real-time tasks with our JSS

  • These tasks are contrived, but will give you an idea of the possibilities

  • As we go, we'll take some tangents, looking a little more deeply into ruby & ruby-jss

  • I do these kinds of one-off tasks frequently in my day-to-day work

Simple Lists of Static and Smart Computer Groups

  • The Classic API provides 'summary-lists' of objects in Jamf Pro

  • They give you a little bit of identifying info about the objects available

  • ruby-jss gives you access to these summary-lists like this

JSS::ComputerGroup.all.sample
  • Lets talk just a moment about ruby itself by looking at that line bit by bit

  • ComputerGroup is a 'class' defined in the JSS module

    • A 'class' in ruby is a 'kind of thing' - this one represents Computer Groups in Jamf Pro
  • Most JSS classes have a method called 'all'

    • A 'method' is like a 'function', a command that you send to something, usually with a dot between them
  • The 'all' method returns an array of hashes

  • An 'array' is a collection of things in a given order, like a list

    • Access things in the array by their numeric position in the list
  • A 'hash' is a collection of things that are labeled, or 'keyed'

    • Access things in the hash by their key
  • In ruby, arrays have a method called 'sample', it returns a random item from the array

  • So the code we pasted gives us a hash, randomly chosen from the array we got from ComputerGroup.all

  • This hash has three things in it

    • ids , the same one seen in the URL for the objects page in Jamf Pro
    • names
    • is_smart
  • To see the whole array, try this

pp JSS::ComputerGroup.all ;0
  • The pp at the beginning and the ;0 at the end make it easier to read in irb.

  • Lets loop thru this array and print the group names

  • While ruby has 'for loops', no one uses them, there are easier ways

  • The simplest is the each method

JSS::ComputerGroup.all.each { |grp_data| puts grp_data[:name] } ;0
  • each is an 'iterator' method, it loops through ('iterates over') the items in a collection

    • the items are put into a temporary variable
    • That variable is used in the block of code
  • The block of code we're using is in the curly-braces:

    • { |grp_data| puts grp_data[:name] }
  • Between the vertical lines is the name of the temp. variable that will hold each item

    • grp_data will hold each hash one after the other
  • puts is the like echo in bash, it prints something to stdout with a newline

  • Lets be more selective and print out just the static groups, with their ids

JSS::ComputerGroup.all.each do |grp_data|
  next if grp_data[:is_smart]
  puts "Static Group Name: #{grp_data[:name]}, ID:  #{grp_data[:id]}"
end ;0
  • Now the code block that we're passing to each has two lines to execute

    • So instead of curly braces, we use do and end
  • Inside the code block

    • the first line looks at the :is_smart key of the current hash
    • If it's true, next skips to the next item in the array, nothing is printed
  • The second line prints a string containing the name and id

    • See how embedding stuff into strings is similar to bash
      • This happens when the string is double-quoted
      • The embedded value is inside #{}
  • That's a totally valid way to do that, but ruby-jss has some shortcuts

  • Let's list the smart groups:

JSS::ComputerGroup.all_smart.each { |grp_data| puts grp_data[:name] } ;0
  • The group classes have shortcuts all_smart and all_static

    • they filter the array you get from all
  • Other classes have other shortcuts that filter the all array

  • Let's look at a few

pp JSS::MobileDevice.all_supervised ;0

pp JSS::DistributionPoint.all_names ;0

pp JSS::MobileDevice.all_wifi_mac_addresses ;0
  • These methods are just manipulating the array you get from all

  • Because the all arrays are used often, they are cached in RAM the first time they are used

  • the cache is always used unless you tell one of the all methods to refresh

  • Here's the same list we just looked at:

    • When we run it again, its fast
    • When we provide :refresh, its slower
pp JSS::MobileDevice.all_wifi_mac_addresses ;0

pp JSS::MobileDevice.all_wifi_mac_addresses :refresh ;0

Next Page

A formatted report

  • In Jamf Pro you can create some reports

  • But occasionally you'll want a report you can't do in the UI

  • For example, a report of:

    • device name, serial, and username
    • for all iPads that are
      • managed & supervised
      • whose users also have a managed Mac laptop
  • Let's use ruby-jss to give us that info

  • First we need a list of all the managed laptops:

mgd_laptops = JSS::Computer.all_laptops.select { |macinfo| macinfo[:managed] } ;0
  • mgd_laptops is a variable in which we'll store some data to use later

    • You assign a value to a variable using =
  • The all_laptops method on the JSS::Computer class gives us the array of summary-hashs for the laptops

  • But we only want the managed ones, so we use the select iterator to filter the list

    • Like each, select loops thru and passes each item of the array into a block of code
    • select remembers the ones where the block evaluated to true
    • Our code block just looks at the :managed value in each hash, which contains true or false
    • select then returns a new array, with only the hashes that were true
  • We store the filtered array in our variable to use in the next step

  • Look at the contents of mdg_laptops and how many laptops it contains:

pp mgd_laptops ;0

mgd_laptops.size
  • Thats an array of hashes, but we really want an array of usernames
mgd_laptop_users = mgd_laptops.map { |macinfo| macinfo[:username] }  ;0
  • We do this with map, another iterator

    • It loops thru the array, building a new array with values calculated in the code block
      • The new array is 'mapped' to the original array
    • The map method returns the new array
    • Our code block is just extracting the :username value from each hash
  • We store the array of usernames in the variable mgd_laptop_users to use later

  • Here it is:

pp mgd_laptop_users ;0
  • Lets get our list of iPads, but only those that are managed and supervised
mgd_supd_ipads = JSS::MobileDevice.all_ipads.select { |ipad| ipad[:managed] && ipad[:supervised] } ;0
# => 0
  • We use select again, on the array of all_ipads

    • we select the hashes where both managed and supervised are true
  • We save the filtered array in the variable mgd_supd_ipads

  • Now we can print our report:

mgd_supd_ipads.each do |ipad_data|
  next unless mgd_laptop_users.include? ipad_data[:username]
  puts "iPad: '#{ipad_data[:name]}' SN: '#{ipad_data[:serial_number]}' User: '#{ipad_data[:username]}' has a managed Mac laptop"
end ;0
  • each loops through the managed, supervised ipads

  • next skips the ipad unless its user's name is in the array of laptop usernames

    • Arrays have a method include? that returns true or false
  • Then we print a line of output


Next Page

  • Let's get a report of all network segments with specified distribution points

  • Alas, the list data from NetworkSegment.all doesn't show that

pp JSS::NetworkSegment.all ;0
  • For any other info about a network segment, we must fetch the full object from the API

  • It's slower, but gives us far more info to work with

JSS::NetworkSegment.all_ids.each do |ns_id|
  net_seg = JSS::NetworkSegment.fetch id: ns_id
  next unless net_seg.distribution_point
  puts "Name: '#{net_seg.name}'\n  Dist. Point: '#{net_seg.distribution_point}'"
  sleep (rand + 1)
end ;0
  • Use each to loop thru the ids of all network segments

  • Each time thru, we use the id to fetch the full network segment using the fetch method

    • We store it in the variable net_seg
  • net_seg now contains an 'instance' of the class JSS::NetworkSegment

    • Instances of classes have their own methods, separate from the methods of the class itself
  • NetworkSegment objects have a distribution_point method

  • next skips those that have no distribution point defined

  • Then print out two lines with the name and the distribution point

    • inside double-quotes, \n means a return character
  • And finally, we pause for some random amount of time 0-2 seconds

    • just for this lab, because otherwise we might swamp the API server

Next Page

  • Let's create a static group

    • Smart groups would take too much time right now
  • Invent a unique name for your group, and store it in a variable:

    • Use something that no one else in this room will use
    • Store it in the variable grp_name
grp_name = 'my-awesome-computer-group'
  • Now create our group object in ruby and store it in a variable
my_grp = JSS::ComputerGroup.make name: grp_name, type: :static ;0

pp my_grp ;0
  • To create new objects in Jamf Pro, use the make method for the class you want

    • You must always provide a name:
    • Group objects need the type:, either :static or :smart
  • This creates a local instance of the JSS::ComputerGroup class

    • It only exists in memory
  • To create it in Jamf Pro, use save

my_grp_id = my_grp.save
  • save creates or updates an object on the server

  • Saving an object returns its id number, which we're storing in a variable

Creating Five Computers

  • You rarely need to create computer records using the API

  • We need to do that, so we each have our own computer records

  • These will be tiny records, just enough to do what we need

    • Computer objects have LOTS of data in them, more than we can look at today
  • pick another unique name to use as the base computer names:

    • Choose something unique
    • Store it in comp_base_name
comp_base_name = 'my-awesome-computer'
  • Make an empty array for the ids of the computers we create, we'll use them later:
new_comp_ids = []
  • Now do a loop 5 times and make new computer records:
5.times do |pass_num|
  comp_name = "#{comp_base_name}-#{pass_num}"

  comp_sn = comp_name + "-sn"

  comp_udid = `uuidgen`.chomp

  comp = JSS::Computer.make name: comp_name, sn: comp_sn, udid: comp_udid; 0

  new_comp_ids << comp.save

  puts "Created Computer '#{comp_name}'"
  puts "  id: #{new_comp_ids.last}"
  puts "  sn: #{comp_sn}"
  puts "  udid: #{comp_udid}"
end
  • Oh look! An iterator!

    • They aren't just for arrays, times works with integers.
  • Here's what the code block does

  • We use the base-name and pass-number thru the loop to create a unique name and fake serialnumber

  • The udid comes from the shell command uuidgen

    • Like in bash, backticks run shell commands and return the output
    • chomp removes the trailing newline
  • Then we make our local ruby Computer instance

    • Again, until we save it, it only exists in local memory, not in the JSS
  • In one step, we save it to the JSS, and append the new id number to the array we created above

    • << appends to the end of an array
  • Finally, we print some lines about it

  • To see that your computers were created, try this:

pp JSS::Computer.all_names(:refresh).select { |name| name.start_with? comp_base_name } ;0

Next Page

Working with static groups

  • Now we can add the computers we just created to the static group we just created

  • Two ways to change group membership:

  1. Add or remove members individually, then save the changes
  • We'll do that later in a script
  1. The change_membership method
    • adds and removes members directly and immediately
    • doesn't require a save
    • is both a class and instance method
      • so you don't have to fetch an instance first
  • Here's how to change_membership on our instance:
my_grp.change_membership add_members: new_comp_ids
  • The add_members: parameter takes an array of identifiers

    • We're using the ids of computers stored in new_comp_ids
  • The changes are immediately applied to Jamf Pro

  • You can also provide a remove_members: parameter to add and remove at the same time

  • Error = add something that doesn't exist


Next Page

Modifying extension attribute values

  • Lets change an extension attribute for all members of our group

  • Put the ext. attr. name into a variable

ea_name = 'JNUC-2019-LabUser'
  • put your desired value into a variable
ea_value = 'Jeaux Bleaux'
  • member_ids on the group give an array of id numbers

  • loop through the ids, to work with each computer

my_grp.member_ids.each do |comp_id|
  computer = JSS::Computer.fetch id: comp_id

  orig_val = computer.ext_attrs[ea_name]

  computer.set_ext_attr ea_name, ea_value

  computer.save

  puts "Changed EA '#{ea_name}' from '#{orig_val}' to '#{ea_value}' for computer '#{computer.name}' "
end
  • In the loop, fetch the Computer by id

  • Store the original value of the EA in a variable, for reporting

  • Set the EA to the desired value

  • Save changes for this computer

  • Print out a line saying what we did

  • Check our work, re-fetch one of the computers, and look at the value

comp_to_check = JSS::Computer.fetch id: my_grp.member_ids.sample ;0

comp_to_check.ext_attrs[ea_name]

Next Page

  • Now delete our 5 computers

  • 2 ways to delete API objects in ruby-jss

  1. Already fetched? Just call its delete method
  • Delete the computer we just fetched a moment ago:
comp_to_check.delete
  1. If you haven't fetched yet, use the delete class method
  • It takes an array of ids, which we happen to have already in group.member_ids

  • This is faster if you haven't fetched, because fetching is slow

JSS::Computer.delete my_grp.member_ids
  • Returns an array of the ids that it did not delete, because they didn't exist

  • In this case, the id we deleted above


Next Page

A Saved Script

  • If there's no time, look thru the code, and I'll run it on my machine

Writing a script to use over and over

  • Lets save some ruby into a script we can run whenever we need

  • In our JSS we already have:

    • A user extension aattribute that marks some users as VIPs
    • A smart user group of those users
  • We want:

    • A Computer Group of computers assigned to those VIPs
    • A Mobile Device group of devices assigned to those VIPs
  • In the Jamf UI, you can't do this

    • create computer or device smart groups based on a user extension attribute
  • This script maintains our static Computer and Device groups nased on user group

  • To make this work now, we need our own computer and device groups

  • The user group and user EA already exist

  • Create your groups with unique names:

JSS::ComputerGroup.make(name: 'ChrissVIP-Computers', type: :static).save ;0

JSS::MobileDeviceGroup.make(name: 'ChrissVIP-Devices', type: :static).save ;0
  • We're using make and save on one line

  • Needs parentheses around the parameters for make

    • otherwise ruby wouldn't understand the whole line.
  • Open a new terminal window

    • Leave your irb terminal open
  • Open a text editor and paste in the script below

  • Change COMPUTER_GROUP and DEVICE_GROUP values to the names of the groups you just created.

    • Make sure to stay inside the quotes
#!/usr/bin/ruby
require 'ruby-jss'

# CHANGE THE NAMES IN THE QUOTES TO SOMETHING UNIQUE
COMPUTER_GROUP = 'ChrissVIP-Computers'
DEVICE_GROUP = 'ChrissVIP-Devices'

# Connect to the Classic API
JSS.api.connect server: 'tryitout.jamfcloud.com', user: 'jnuc2019', pw: :prompt

# Fetch the Smart User Group
# It's based on User Ext Attrib 'vip' containing 'yes'
user_group = JSS::UserGroup.fetch name: 'VIPs'

# Fetch the Static Groups
comp_group = JSS::ComputerGroup.fetch name: COMPUTER_GROUP
dev_group = JSS::MobileDeviceGroup.fetch name: DEVICE_GROUP

# Clear out the group memberships in the static groups
comp_group.clear
dev_group.clear

# Loop thru the user ids in the user group
user_group.member_ids.each do |user_id|

  # Fetch the user object
  user = JSS::User.fetch id: user_id

  puts "--- Processing user: #{user.name}"

  # Loop thru the user's computers, adding each to the computer group
  user.computers.each do |comp|
    comp_group.add_member comp[:id]
    puts "  ..Added Computer '#{comp[:name]}''"
  end # user.computers.each

  # Loop thru the user's devices, adding each to the device group
  user.mobile_devices.each do |dev|
    dev_group.add_member dev[:id]
    puts "  ..Added Device '#{dev[:name]}'"
  end # user.mobile_devices

end # user_group.member_ids.each

puts '--- Done with users'

# add_member doesn't save immediately like change_membership
# so we need to save the group changes
comp_group.save
puts '--- Saved Computer Group'

dev_group.save
puts '--- Saved Mobile Device Group'

# Finito
puts 'All Done!'
  • Save the script somewhere

  • Make it executable chmod 755 /path/to/script

  • Before we run it, read through it, its well commented

  • Run it in the new terminal: /path/to/script

  • Use anything for the password

  • When finished, back to irb to check if the groups have members

pp JSS::ComputerGroup.fetch(name: 'ChrissVIP-Computers').member_names ;0

pp JSS::MobileDeviceGroup.fetch(name: 'ChrissVIP-Devices').member_names ;0
  • A useful tool, in 27 lines of readable code.

  • You could run this automatically at intervals

  • In reality, this would be more robust

    • Catching & reporting errors
    • Maybe Adding and removing the members as needed, not clearing and re-adding them every time
    • Output would go to a log file

BACK TO SLIDES

Clone this wiki locally