Class: JSS::Client

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

Overview

jamf client computer

Constant Summary collapse

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'
SUPPORT_BIN_FOLDER =

The bin folder inside the Jamf support folder

JAMF_SUPPORT_FOLDER + 'bin'
USR_LOCAL_BIN_FOLDER =

The bin folder with the jamf binary and a few other things

Pathname.new '/usr/local/jamf/bin'
CONSOLE_USERS_SCUTIL_CMD =

This command gives raw info about console users

'echo "show State:/Users/ConsoleUser" | /usr/sbin/scutil'.freeze
ROOT_USER =

ignore console user = root (loginwindow)

'root'.freeze
LOGINWINDOW_USER =

ignore primary console user loginwindow

'loginwindow'.freeze
SELF_SERVICE_EXECUTABLE_END =

The end of the path to the Self Service Executable. Used to figure out who's running Self Service.app

'/Self Service.app/Contents/MacOS/Self Service'.freeze
PS_USER_COMM =

the ps command used to figure out who's running Self Service

'ps -A -o user,comm'.freeze
USER_PREFS_BYHOST_FOLDER =

the path to a users byhost folder from home

'Library/Preferences/ByHost/'
ORIG_JAMF_BINARY =

The Pathname to the jamf binary executable Before SIP (macOS 10.10 and below)

Pathname.new '/usr/sbin/jamf'
SIP_JAMF_BINARY =

The Pathname to the jamf binary executable After SIP (OS X 10.11 and above)

USR_LOCAL_BIN_FOLDER + 'jamf'
JAMF_BINARY =

The path to the jamf binary

SIP_JAMF_BINARY.executable? ? SIP_JAMF_BINARY : ORIG_JAMF_BINARY
ROOTLESS_JAMF_COMMANDS =

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

i[
  about
  checkJSSConnection
  getARDFields
  getComputerName
  help
  listUsers
  version
].freeze
JAMF_VERBOSE_OPT =

the option that makes the jamf binary verbose

' -verbose'.freeze
JAMF_HELPER =

The Pathname to the jamfHelper executable

SUPPORT_BIN_FOLDER + '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

i[right left center justified natural].freeze
MGMT_ACTION =

The Pathname to the Management Action executable

SUPPORT_BIN_FOLDER + 'Management Action.app/Contents/MacOS/Management Action'

Class Method Summary collapse

Class Method Details

.build_jamf_command(command, args) ⇒ Object


102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/jss/client/jamf_binary.rb', line 102

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

.console_userObject

alias for primary_console_user


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

def self.console_user
  primary_console_user
end

.console_usersArray<String>

Who's currently got an active GUI session? - might be more than one if Fast User Switching is in use.


236
237
238
239
240
241
# File 'lib/jss/client.rb', line 236

def self.console_users
  output = `#{CONSOLE_USERS_SCUTIL_CMD}`
  userlines = output.lines.select { |l| l =~ /SessionUserNameKey\s*:/ }
  userlines.map! { |ul| ul.split(':').last.strip }
  userlines.reject { |un| un == ROOT_USER }
end

.do_not_disturb?(user = nil) ⇒ Boolean?


275
276
277
278
279
280
281
# File 'lib/jss/client.rb', line 275

def self.do_not_disturb?(user = nil)
  home = user ? homedir(user) : Dir.home
  myudid = udid
  nc_prefs_file = Pathname.new "#{home}/#{USER_PREFS_BYHOST_FOLDER}/com.apple.notificationcenterui.#{myudid}.plist"
  return nil unless nc_prefs_file.readable?
  JSS.parse_plist(nc_prefs_file)['doNotDisturb']
end

.execute_jamf(cmd, verbose) ⇒ Object


115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/jss/client/jamf_binary.rb', line 115

def self.execute_jamf(cmd, verbose)
  puts "Running: #{cmd}" if verbose
  output = ''
  IO.popen("#{cmd} 2>&1") do |proc|
    loop do
      line = proc.gets
      break unless line
      output << line
      puts line if verbose
    end
  end
  output.force_encoding('UTF-8')
  output
end

.force_alertsObject

Skipping all the force-alerts stuff until we figure out cleaner ways to do it in 10.13+ The plan is to be able to make the NotificationCenter notification be an 'alert' (which stays visible til the user clicks) or a 'banner' (which vanishes in a few seconds), regardless of the user's setting in the NC prefs.


71
72
73
74
75
76
77
78
79
# File 'lib/jss/client/management_action.rb', line 71

def self.force_alerts
  orig_flags = {}
  console_users.each do |user|
    orig_flags[user] = set_mgmt_action_ncprefs_flags user, NC_ALERT_STYLE_FLAGS, hup: false
  end
  system HUP_NOTIF_CTR_CMD unless orig_flags.empty?
  sleep 1
  orig_flags
end

.hardware_dataHash

The parsed HardwareDataType output from system_profiler


226
227
228
229
# File 'lib/jss/client.rb', line 226

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

.homedir(user) ⇒ Pathname?

The home dir of the specified user, nil if no homedir in local dscl.


290
291
292
293
# File 'lib/jss/client.rb', line 290

def self.homedir(user)
  dir = `/usr/bin/dscl . -read /Users/#{user} NFSHomeDirectory 2>/dev/null`.chomp.split(': ').last
  dir ? Pathname.new(dir) : nil
end

.installed?Boolean

Is the jamf binary installed?


115
116
117
# File 'lib/jss/client.rb', line 115

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:


178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/jss/client/jamf_helper.rb', line 178

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

an empty hash if not


173
174
175
176
# File 'lib/jss/client.rb', line 173

def self.jamf_plist
  return {} unless JAMF_PLIST.file?
  JSS.parse_plist JAMF_PLIST
end

.jamf_versionString?

What version of the jamf binary is installed?


123
124
125
# File 'lib/jss/client.rb', line 123

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

.jss_available?Boolean

Is the JSS available right now?


191
192
193
194
# File 'lib/jss/client.rb', line 191

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


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

def self.jss_port
  jss_url
  @port
end

.jss_protocolString

The protocol for JSS connections for this client


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

def self.jss_protocol
  jss_url
  @protocol
end

.jss_recordJSS::Computer?

The JSS::Computer object for this computer


200
201
202
203
204
# File 'lib/jss/client.rb', line 200

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

.jss_serverString

The JSS server hostname for this client


145
146
147
148
# File 'lib/jss/client.rb', line 145

def self.jss_server
  jss_url
  @server
end

.jss_urlString

the URL to the jss for this client


131
132
133
134
135
136
137
138
139
# File 'lib/jss/client.rb', line 131

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

.management_action(msg, title: nil, subtitle: nil, delay: 0) ⇒ Object

class Methods


46
47
48
49
50
51
52
53
54
55
# File 'lib/jss/client/management_action.rb', line 46

def self.management_action(msg, title: nil, subtitle: nil, delay: 0)
  raise JSS::InvalidDataError, 'delay: must be a non-negative integer.' unless delay.is_a?(Integer) && delay > -1

  cmd = Shellwords.escape MGMT_ACTION.to_s
  cmd << " -message #{Shellwords.escape msg.to_s}"
  cmd << " -title #{Shellwords.escape title.to_s}" if title
  cmd << " -subtitle #{Shellwords.escape subtitle.to_s}" if subtitle
  cmd << " -deliverydelay #{Shellwords.escape delay}" if delay > 0
  `#{cmd} 2>&1`
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/


97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/jss/client.rb', line 97

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

.nc_notify(msg, title: nil, subtitle: nil, delay: 0) ⇒ Object

an alias of management_action


58
59
60
# File 'lib/jss/client/management_action.rb', line 58

def self.nc_notify(msg, title: nil, subtitle: nil, delay: 0)
  management_action(msg, title: title, subtitle: subtitle, delay: delay)
end

.primary_console_userString?

Which console user is using the primary GUI console? Returns nil if the primary GUI console is at the login window.


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

def self.primary_console_user
  `#{CONSOLE_USERS_SCUTIL_CMD}` =~ /^\s*Name : (\S+)$/
  user = Regexp.last_match(1)
  user == LOGINWINDOW_USER ? nil : user
end

.receiptsArray<Pathname>

All the JAMF receipts on this client


182
183
184
185
# File 'lib/jss/client.rb', line 182

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

.restore_alerts(orig_flags) ⇒ Object


81
82
83
84
85
86
# File 'lib/jss/client/management_action.rb', line 81

def self.restore_alerts(orig_flags)
  orig_flags.each do |user, flags|
    set_mgmt_action_ncprefs_flags user, flags, hup: false
  end
  system HUP_NOTIF_CTR_CMD unless orig_flags.empty?
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 $CHILD_STATUS 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:


90
91
92
93
94
95
96
97
98
# File 'lib/jss/client/jamf_binary.rb', line 90

def self.run_jamf(command, args = nil, verbose = false)
  raise JSS::UnmanagedError, 'The jamf binary is not installed on this computer.' unless installed?
  unless ROOTLESS_JAMF_COMMANDS.include?(command.to_sym) || JSS.superuser?
    raise JSS::UnsupportedError, 'You must have root privileges to run that jamf binary command'
  end
  cmd = build_jamf_command command, args
  cmd += " #{JAMF_VERBOSE_OPT}" if verbose && !cmd.include?(JAMF_VERBOSE_OPT)
  execute_jamf cmd, verbose
end

.self_service_usersArray<String>

Who's currently running Self Service.app? - might be more than one if Fast User Switching is in use.


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

def self.self_service_users
  ss_userlines = `#{PS_USER_COMM}`.lines.select { |l| l.include? SELF_SERVICE_EXECUTABLE_END }
  ss_userlines.map { |ssl| ssl.split(' ').first }
end

.serial_numberString

The serial number for this computer via system_profiler


218
219
220
# File 'lib/jss/client.rb', line 218

def self.serial_number
  hardware_data['serial_number']
end

.set_mgmt_action_ncprefs_flags(user, flags, hup: true) ⇒ Integer

set the NotificationCenter option flags for a user flags = an integer.

Doesn't seem to work in 10.13, so ignore this for now.


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/jss/client/management_action.rb', line 95

def self.set_mgmt_action_ncprefs_flags(user, flags, hup: true)
  plist = Pathname.new "/Users/#{user}/Library/Preferences/#{NCPREFS_DOMAIN}.plist"
  prefs = JSS.parse_plist plist
  mgmt_action_setting = prefs['apps'].select { |a| a['bundle-id'] == MGMT_ACTION_BUNDLE_ID }.first
  if mgmt_action_setting
    orig_flags = mgmt_action_setting['flags']
    mgmt_action_setting['flags'] = flags
  else
    orig_flags = flags
    prefs['apps'] << { 'bundle-id' => MGMT_ACTION_BUNDLE_ID, 'flags' => flags }
  end
  # system "/usr/bin/defaults write #{NCPREFS_DOMAIN} '#{prefs.to_plist}'"
  plist.open('w') { |f| f.write prefs.to_plist }
  system HUP_NOTIF_CTR_CMD if hup
  orig_flags
end

.udidString

The UUID for this computer via system_profiler


210
211
212
# File 'lib/jss/client.rb', line 210

def self.udid
  hardware_data['platform_UUID']
end