Class: JSS::Client

Inherits:
Object show all
Defined in:
lib/jss/client.rb,
lib/jss.rb

Overview

This class represents a Casper/JSS Client computer, on which this code is running.

Since the class represents the current machine, there’s no need to make an instance of it, all methods are class methods.

At the moment, only Macintosh computers are supported.

Constant Summary collapse

ORIG_JAMF_BINARY =

The Pathname to the jamf binary executable As of El Capitan (OS X 10.11) the location has moved.

Pathname.new '/usr/sbin/jamf'
ELCAP_JAMF_BINARY =
Pathname.new '/usr/local/jamf/bin/jamf'
JAMF_BINARY =
ELCAP_JAMF_BINARY.executable? ? ELCAP_JAMF_BINARY : ORIG_JAMF_BINARY
JAMF_HELPER =

The Pathname to the jamfHelper executable

Pathname.new '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper'
JAMF_HELPER_WINDOW_TYPES =

The window_type options for jamfHelper

{
  hud: 'hud',
  utility: 'utility',
  util: 'utility',
  full_screen: 'fs',
  fs: 'fs'
}.freeze
JAMF_HELPER_WINDOW_POSITIONS =

The possible window positions for jamfHelper

[nil, :ul, :ll, :ur, :lr].freeze
JAMF_HELPER_BUTTONS =

The available buttons in jamfHelper

[1, 2].freeze
JAMF_HELPER_ALIGNMENTS =

The possible alignment positions in jamfHelper

[:right, :left, :center, :justified, :natural].freeze
JAMF_PLIST =

The Pathname to the preferences plist used by the jamf binary

Pathname.new '/Library/Preferences/com.jamfsoftware.jamf.plist'
JAMF_SUPPORT_FOLDER =

The Pathname to the JAMF support folder

Pathname.new '/Library/Application Support/JAMF'
RECEIPTS_FOLDER =

The JAMF receipts folder, where package installs are tracked.

JAMF_SUPPORT_FOLDER + 'Receipts'
DOWNLOADS_FOLDER =

The JAMF downloads folder

JAMF_SUPPORT_FOLDER + 'Downloads'
ROOTLESS_JAMF_COMMANDS =

These jamf commands don’t need root privs (most do)

[
  :about,
  :checkJSSConnection,
  :getARDFields,
  :getComputerName,
  :help,
  :listUsers,
  :version
].freeze

Class Method Summary collapse

Class Method Details

.console_userString?

Who’s logged in to the console right now?



139
140
141
142
143
144
145
146
147
148
149
# File 'lib/jss/client.rb', line 139

def self.console_user
  cmd = '/usr/sbin/scutil'
  qry = 'show State:/Users/ConsoleUser'
  Open3.popen2e(cmd) do |cmdin, cmdouterr, _wait_thr|
    cmdin.puts qry
    cmdin.close
    out = cmdouterr.read
    user = out.lines.select { |l| l =~ /^\s+Name\s*:/ }.first.to_s.split(/\s*:\s*/).last
    return user.nil? ? user : user.chomp
  end # do
end

.hardware_dataHash

The parsed HardwareDataType output from system_profiler



265
266
267
268
# File 'lib/jss/client.rb', line 265

def self.hardware_data
  raw = `/usr/sbin/system_profiler SPHardwareDataType -xml 2>/dev/null`
  Plist.parse_xml(raw)[0]['_items'][0]
end

.installed?Boolean

Is the jamf binary installed?



155
156
157
# File 'lib/jss/client.rb', line 155

def self.installed?
  JAMF_BINARY.executable?
end

.jamf_helper(window_type = :hud, opts = {}) ⇒ Integer

Note:

the -startlaunchd and -kill options are not available in this implementation, since they don’t work at the moment (casper 9.4). -startlaunchd seems to be required to NOT use launchd, and when it’s ommited, an error is generated about the launchd plist permissions being incorrect.

A wrapper for the jamfHelper command, which can display a window on the client machine.

The first parameter must be a symbol defining what kind of window to display. The options are

  • :hud - creates an Apple “Heads Up Display” style window

  • :utility or :util - creates an Apple “Utility” style window

  • :fs or :full_screen or :fullscreen - creates a full screen window that restricts all user input WARNING: Remote access must be used to unlock machines in this mode

The remaining options Hash can contain any of the options listed. See below for descriptions.

The value returned is the Integer exitstatus/stdout (both are the same) of the jamfHelper command. The meanings of those integers are:

  • 0 - Button 1 was clicked

  • 1 - The Jamf Helper was unable to launch

  • 2 - Button 2 was clicked

  • 3 - Process was started as a launchd task

  • XX1 - Button 1 was clicked with a value of XX seconds selected in the drop-down

  • XX2 - Button 2 was clicked with a value of XX seconds selected in the drop-down

  • 239 - The exit button was clicked

  • 240 - The “ProductVersion” in sw_vers did not return 10.5.X, 10.6.X or 10.7.X

  • 243 - The window timed-out with no buttons on the screen

  • 250 - Bad “-windowType”

  • 254 - Cancel button was select with delay option present

  • 255 - No “-windowType”

If the :abandon_process option is given, the integer returned is the Process ID of the abondoned process running jamfHelper.

See also /Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -help

Options Hash (opts):

  • :window_position (Symbol, nil)

    one of [ nil, :ul, :ll. :ur, :lr ] Positions window in the upper right, upper left, lower right or lower left of the user’s screen If no input is given, the window defaults to the center of the screen

  • :title (String)

    Sets the window’s title to the specified string

  • :heading (String)

    Sets the heading of the window to the specified string

  • :align_heading (Symbol)

    one of [:right, :left, :center, :justified, :natural] Aligns the heading to the specified alignment

  • :description (String)

    Sets the main contents of the window to the specified string

  • :align_description (Symbol)

    one of [:right, :left, :center, :justified, :natural] Aligns the description to the specified alignment

  • :icon (String, Pathname)

    Sets the windows image field to the image located at the specified path

  • :icon_size (Integer)

    Changes the image frame to the specified pixel size

  • :full_screen_icon (any value)

    Scales the “icon” to the full size of the window. Note: Only available in full screen mode

  • :button1 (String)

    Creates a button with the specified label

  • :button2 (String)

    Creates a second button with the specified label

  • :default_button (Integer)

    either 1 or 2 Sets the default button of the window to the specified button. The Default Button will respond to “return”

  • :cancel_button (Integer)

    either 1 or 2 Sets the cancel button of the window to the specified button. The Cancel Button will respond to “escape”

  • :timeout (Integer)

    Causes the window to timeout after the specified amount of seconds Note: The timeout will cause the default button, button 1 or button 2 to be selected (in that order)

  • :show_delay_options (String, Array<Integer>)

    A String of comma-separated Integers, or an Array of Integers. Enables the “Delay Options Mode”. The window will display a dropdown with the values passed through the string

  • :countdown (any value)

    Displays a string notifying the user when the window will time out

  • :align_countdown (Symbol)

    one of [:right, :left, :center, :justified, :natural] Aligns the countdown to the specified alignment

  • :lock_hud (Boolean)

    Removes the ability to exit the HUD by selecting the close button

  • :abandon_process (Boolean)

    Abandon the jamfHelper process so that your code can exit. This is mostly used so that a policy can finish while a dialog is waiting (possibly forever) for user response. When true, the returned value is the process id of the abandoned jamfHelper process.

  • :output_file (String, Pathname)

    Save the output of jamfHelper (the exit code) into this file. This is useful when using abandon_process. The output file can be examined later to see what happened. If this option is not provided, no output is saved.

  • :arg_string (String)

    The jamfHelper commandline args as a single String, the way you’d specify them in a shell. This is appended to any Ruby options provided when calling the method. So calling:

    JSS::Client.jamf_helper :hud, title: 'This is a title', arg_string: '-heading "this is a heading"'
    

    will run

    jamfHelper -windowType hud -title 'this is a title' -heading "this is a heading"
    

    When using this, be careful not to specify the windowType, since it’s generated by the first, required, parameter of this method.

Raises:



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
# File 'lib/jss/client.rb', line 446

def self.jamf_helper(window_type = :hud, opts = {})
  raise JSS::UnmanagedError, 'The jamfHelper app is not installed properly on this computer.' unless JAMF_HELPER.executable?

  unless JAMF_HELPER_WINDOW_TYPES.include? window_type
    raise JSS::InvalidDataError, "The first parameter must be a window type, one of :#{JAMF_HELPER_WINDOW_TYPES.keys.join(', :')}."
  end

  # start building the arg array

  args = ['-startlaunchd', '-windowType', JAMF_HELPER_WINDOW_TYPES[window_type]]

  opts.keys.each do |opt|
    case opt
    when :window_position
      raise JSS::InvalidDataError, ":window_position must be one of :#{JAMF_HELPER_WINDOW_POSITIONS.join(', :')}." unless \
        JAMF_HELPER_WINDOW_POSITIONS.include? opts[opt].to_sym
      args << '-windowPosition'
      args << opts[opt].to_s

    when :title
      args << '-title'
      args << opts[opt].to_s

    when :heading
      args << '-heading'
      args << opts[opt].to_s

    when :align_heading
      raise JSS::InvalidDataError, ":align_heading must be one of :#{JAMF_HELPER_ALIGNMENTS.join(', :')}." unless \
        JAMF_HELPER_ALIGNMENTS.include? opts[opt].to_sym
      args << '-alignHeading'
      args << opts[opt].to_s

    when :description
      args << '-description'
      args << opts[opt].to_s

    when :align_description
      raise JSS::InvalidDataError, ":align_description must be one of :#{JAMF_HELPER_ALIGNMENTS.join(', :')}." unless \
        JAMF_HELPER_ALIGNMENTS.include? opts[opt].to_sym
      args << '-alignDescription'
      args << opts[opt].to_s

    when :icon
      args << '-icon'
      args << opts[opt].to_s

    when :icon_size
      args << '-iconSize'
      args << opts[opt].to_s

    when :full_screen_icon
      args << '-fullScreenIcon'

    when :button1
      args << '-button1'
      args << opts[opt].to_s

    when :button2
      args << '-button2'
      args << opts[opt].to_s

    when :default_button
      raise JSS::InvalidDataError, ":default_button must be one of #{JAMF_HELPER_BUTTONS.join(', ')}." unless \
        JAMF_HELPER_BUTTONS.include? opts[opt]
      args << '-defaultButton'
      args << opts[opt].to_s

    when :cancel_button
      raise JSS::InvalidDataError, ":cancel_button must be one of #{JAMF_HELPER_BUTTONS.join(', ')}." unless \
        JAMF_HELPER_BUTTONS.include? opts[opt]
      args << '-cancelButton'
      args << opts[opt].to_s

    when :timeout
      args << '-timeout'
      args << opts[opt].to_s

    when :show_delay_options
      args << '-showDelayOptions'
      args << JSS.to_s_and_a(opts[opt])[:arrayform].join(', ')

    when :countdown
      args << '-countdown' if opts[opt]

    when :align_countdown
      raise JSS::InvalidDataError, ":align_countdown must be one of :#{JAMF_HELPER_ALIGNMENTS.join(', :')}." unless \
        JAMF_HELPER_ALIGNMENTS.include? opts[opt].to_sym
      args << '-alignCountdown'
      args << opts[opt].to_s

    when :lock_hud
      args << '-lockHUD' if opts[opt]

    end # case opt
  end # each do opt

  cmd = Shellwords.escape JAMF_HELPER.to_s
  args.each { |arg| cmd << " #{Shellwords.escape arg}" }
  cmd << " #{opts[:arg_string]}" if opts[:arg_string]
  cmd << " > #{Shellwords.escape opts[:output_file]}" if opts[:output_file]

  if opts[:abandon_process]
    pid = Process.fork
    if pid.nil?
      # In child
      exec cmd
    else
      # In parent
      Process.detach(pid)
      pid
    end
  else
    system cmd
    $CHILD_STATUS.exitstatus
  end
end

.jamf_plistHash

The contents of the JAMF plist



212
213
214
215
# File 'lib/jss/client.rb', line 212

def self.jamf_plist
  return {} unless JAMF_PLIST.file?
  Plist.parse_xml `/usr/libexec/PlistBuddy -x -c print #{Shellwords.escape JSS::Client::JAMF_PLIST.to_s}`
end

.jamf_versionString?

What version of the jamf binary is installed?



163
164
165
# File 'lib/jss/client.rb', line 163

def self.jamf_version
  installed? ? run_jamf(:version).chomp.split('=')[1] : nil
end

.jss_available?Boolean

Is the JSS available right now?



230
231
232
233
# File 'lib/jss/client.rb', line 230

def self.jss_available?
  run_jamf :checkJSSConnection, '-retry 1'
  $CHILD_STATUS.exitstatus.zero?
end

.jss_portInteger

The port number for JSS connections for this client



203
204
205
206
# File 'lib/jss/client.rb', line 203

def self.jss_port
  jss_url
  @port
end

.jss_protocolString

The protocol for JSS connections for this client



194
195
196
197
# File 'lib/jss/client.rb', line 194

def self.jss_protocol
  jss_url
  @protocol
end

.jss_recordJSS::Computer?

The JSS::Computer object for this computer



239
240
241
242
243
# File 'lib/jss/client.rb', line 239

def self.jss_record
  JSS::Computer.new udid: udid
rescue JSS::NoSuchItemError
  nil
end

.jss_serverString

The JSS server hostname for this client



185
186
187
188
# File 'lib/jss/client.rb', line 185

def self.jss_server
  jss_url
  @server
end

.jss_urlString

the URL to the jss for this client



171
172
173
174
175
176
177
178
179
# File 'lib/jss/client.rb', line 171

def self.jss_url
  @url = jamf_plist['jss_url']
  return nil if @url.nil?
  @url =~ %r{(https?)://(.+):(\d+)/}
  @protocol = Regexp.last_match(1)
  @server = Regexp.last_match(2)
  @port = Regexp.last_match(3)
  @url
end

.my_ip_addressString

Get the current IP address as a String.

This handy code doesn’t acutally make a UDP connection, it just starts to set up the connection, then uses that to get the local IP.

Lifted gratefully from coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/



121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/jss/client.rb', line 121

def self.my_ip_address
  # turn off reverse DNS resolution temporarily
  # @note the 'socket' library has already been required by 'rest-client'
  orig = Socket.do_not_reverse_lookup
  Socket.do_not_reverse_lookup = true

  UDPSocket.open do |s|
    s.connect '192.168.0.0', 1
    s.addr.last
  end
ensure
  Socket.do_not_reverse_lookup = orig
end

.receiptsArray<Pathname>

All the JAMF receipts on this client



221
222
223
224
# File 'lib/jss/client.rb', line 221

def self.receipts
  raise JSS::NoSuchItemError, "The JAMF Receipts folder doesn't exist on this computer." unless RECEIPTS_FOLDER.exist?
  RECEIPTS_FOLDER.children.select(&:file?)
end

.run_jamf(command, args = nil, verbose = false) ⇒ String

Note:

Most jamf commands require superuser/root privileges.

Run an arbitrary jamf binary command.

The details of the Process::Status for the jamf binary process can be captured from $? immediately after calling. (See Process::Status)

Examples:

These two are equivalent:

  JSS::Client.run_jamf "recon", "-assetTag 12345 -department 'IT Support'"

  JSS::Client.run_jamf :recon, ['-assetTag', '12345', '-department', 'IT Support'"]

Raises:



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/jss/client.rb', line 297

def self.run_jamf(command, args = nil, verbose = false)
  raise JSS::UnmanagedError, 'The jamf binary is not installed on this computer.' unless installed?
  raise JSS::UnsupportedError, 'You must have root privileges to run that jamf binary command' unless \
    ROOTLESS_JAMF_COMMANDS.include?(command.to_sym) || JSS.superuser?

  cmd = case args
        when nil
          "#{JAMF_BINARY} #{command}"
        when String
          "#{JAMF_BINARY} #{command} #{args}"
        when Array
          ([JAMF_BINARY.to_s, command] + args).join(' ').to_s
        else
          raise JSS::InvalidDataError, 'args must be a String or Array of Strings'
        end # case

  cmd += ' -verbose' if verbose && (!cmd.include? ' -verbose')
  puts "Running: #{cmd}" if verbose

  output = []
  IO.popen("#{cmd} 2>&1") do |proc|
    while line = proc.gets
      output << line
      puts line if verbose
    end
  end
  install_out = output.join('')
  install_out.force_encoding('UTF-8') if install_out.respond_to? :force_encoding
  install_out
end

.serial_numberString

The serial number for this computer via system_profiler



257
258
259
# File 'lib/jss/client.rb', line 257

def self.serial_number
  hardware_data['serial_number']
end

.udidString

The UUID for this computer via system_profiler



249
250
251
# File 'lib/jss/client.rb', line 249

def self.udid
  hardware_data['platform_UUID']
end