-
Notifications
You must be signed in to change notification settings - Fork 30
JNUC2019 Lab Session Presenter
- Open Terminal.app or your preferred terminal
- Run
irb
% irb
irb(main):001:0>
-
'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
=>
- 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.
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
-
-
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
-
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 likeecho
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
andend
- So instead of curly braces, we use
-
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 first line looks at the
-
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
#{}
- See how embedding stuff into strings is similar to bash
-
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
andall_static
- they filter the array you get from
all
- they filter the array you get from
-
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
-
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
=
- You assign a value to a variable using
-
The
all_laptops
method on theJSS::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 totrue
- Our code block just looks at the
:managed
value in each hash, which containstrue
orfalse
-
select
then returns a new array, with only the hashes that were true
- Like
-
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
- It loops thru the array, building a new array with values calculated in the code block
-
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 ofall_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
- Arrays have a method
-
Then we print a line of output
-
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
- We store it in the variable
-
net_seg
now contains an 'instance' of the classJSS::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
- inside double-quotes,
-
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
-
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
- You must always provide a
-
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
-
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.
- They aren't just for arrays,
-
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
-
Now we can add the computers we just created to the static group we just created
-
Two ways to change group membership:
- Add or remove members individually, then save the changes
- We'll do that later in a script
- 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
- We're using the ids of computers stored in
-
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
-
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]
-
Now delete our 5 computers
-
2 ways to delete API objects in ruby-jss
- Already fetched? Just call its
delete
method
- Delete the computer we just fetched a moment ago:
comp_to_check.delete
- 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
- If there's no time, look thru the code, and I'll run it on my machine
-
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
andsave
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