Module: Xolo::Server::Helpers::ClientData

Defined in:
lib/xolo/server/helpers/client_data.rb

Overview

Constants and methods for maintaining the client data package

This is used as a ‘helper’ in the Sinatra server

This means methods here are available in all routes, views, and helpers the Sinatra server app.

The client data package is a Jamf::JPackage that installs a JSON file on all managed Macs. This JSON file contains data about all titles and versions, and any other data that the xolo client needs to know about.

It is updated automatically by the server when titles or versions are changed.

It is used so that the xolo client can know what it needs to know about titles and versions without having to query the server or do anything over a network other than using the jamf binary.

The downside is that the client data package is likely to be somewhat out of date, but that is a tradeoff for the simplicity and security of the client.

The client data package is installed in /Library/Application Support/xolo/client-data.json it contains a JSON object with a ‘titles’ key, which is an object with keys for each title. The data provided is that produced by the Title#to_h and Version#to_h methods.

Constant Summary collapse

CLIENT_DATA_STR =

Constants

'client-data'
CLIENT_DATA_COMPONENT_PACKAGE_FILE =

The name of the package file that installs the xolo-client-data JSON file

"#{CLIENT_DATA_STR}-component.pkg"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.client_data_mutexMutex

A mutex for the client data update process

TODO: use Concrrent Ruby instead of Mutex

Returns:

  • (Mutex)

    the mutex



77
78
79
# File 'lib/xolo/server/helpers/client_data.rb', line 77

def self.client_data_mutex
  @client_data_mutex ||= Mutex.new
end

.extended(extender) ⇒ Object

when this module is extended



67
68
69
# File 'lib/xolo/server/helpers/client_data.rb', line 67

def self.extended(extender)
  Xolo.verbose_extend extender, self
end

.included(includer) ⇒ Object

when this module is included



62
63
64
# File 'lib/xolo/server/helpers/client_data.rb', line 62

def self.included(includer)
  Xolo.verbose_include includer, self
end

Instance Method Details

#build_component_client_data_pkg_file(root_dir, pkg_version, pkg_work_dir) ⇒ Pathname

Build the component install pkg with pkgbuild NOTE: no need to shellescape the paths, since we are using the array version of Open3.capture2e

Returns:

  • (Pathname)

    the path to the new package file



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/xolo/server/helpers/client_data.rb', line 321

def build_component_client_data_pkg_file(root_dir, pkg_version, pkg_work_dir)
  outfile = pkg_work_dir + CLIENT_DATA_COMPONENT_PACKAGE_FILE

  cmd = ['/usr/bin/pkgbuild']
  cmd << '--root'
  cmd << root_dir.to_s
  cmd << '--identifier'
  cmd << client_data_package_identifier
  cmd << '--version'
  cmd << pkg_version
  cmd << '--install-location'
  cmd << '/'
  cmd << '--sign'
  cmd << Xolo::Server.config.pkg_signing_identity
  cmd << '--keychain'
  cmd << Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
  cmd << outfile.to_s

  log_debug "Command to build component pkg '#{CLIENT_DATA_COMPONENT_PACKAGE_FILE}': #{cmd.join(' ')}"

  stdouterr, exit_status = Open3.capture2e(*cmd)
  raise "Error creating #{client_data_package_file}: #{stdouterr}" unless exit_status.success?

  outfile
end

#build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir) ⇒ Pathname

Build the distribution package for the xolo-client-data JSON file NOTE: no need to shellescape the paths, since we are using the array version of Open3.capture2e

Returns:

  • (Pathname)

    the path to the new package file



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/xolo/server/helpers/client_data.rb', line 353

def build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir)
  pkg_file = pkg_work_dir + client_data_package_file

  cmd = ['/usr/bin/productbuild']
  cmd << '--package'
  cmd << component_pkg_file.to_s
  cmd << '--identifier'
  cmd << client_data_package_identifier
  cmd << '--version'
  cmd << pkg_version
  cmd << '--sign'
  cmd << Xolo::Server.config.pkg_signing_identity
  cmd << '--keychain'
  cmd << Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
  cmd << pkg_file.to_s

  log_debug "Command to build distribution pkg '#{client_data_package_file}': #{cmd.join(' ')}"

  stdouterr, exit_status = Open3.capture2e(*cmd)
  raise "Error creating #{client_data_package_file}: #{stdouterr}" unless exit_status.success?

  pkg_file
end

#client_app_sourcePathname

Returns the path to the client executable ‘xolo’ in the ruby gem.

Returns:

  • (Pathname)

    the path to the client executable ‘xolo’ in the ruby gem



407
408
409
410
411
412
413
414
# File 'lib/xolo/server/helpers/client_data.rb', line 407

def client_app_source
  # parent 1 == helpers
  # parent 2 == server
  # parent 3 == xolo
  # parent 4 == lib
  # parent 5 == root
  @client_app ||= Pathname.new(__FILE__).expand_path.parent.parent.parent.parent.parent + 'data' + 'client' + 'xolo'
end

#client_data_auto_policy_nameString

The name of the Jamf::Policy object that installs the xolo-client-data package automatically on all managed Macs NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY

Returns:

  • (String)

    the name of the automatic policy for the client-data package in Jamf Pro



124
125
126
# File 'lib/xolo/server/helpers/client_data.rb', line 124

def client_data_auto_policy_name
  @client_data_auto_policy_name ||= "#{client_data_package_name}-auto"
end

#client_data_filenameString

The name of the client-data JSON file in the xolo-client-data package this is the file that is installed onto managed Macs in /Library/Application Support/xolo/

Returns:

  • (String)

    the name of the client-data JSON file in the package



93
94
95
# File 'lib/xolo/server/helpers/client_data.rb', line 93

def client_data_filename
  @client_data_filename ||= Xolo::Server.config.test_server ? "test-#{CLIENT_DATA_STR}.json" : "#{CLIENT_DATA_STR}.json"
end

#client_data_hashHash

Returns the data to put in the xolo-client-data JSON file.

Returns:

  • (Hash)

    the data to put in the xolo-client-data JSON file



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/xolo/server/helpers/client_data.rb', line 379

def client_data_hash
  cdh = {
    titles: {}
  }
  return cdh if all_titles.empty?

  all_title_objects(refresh: true).each do |title|
    cdh[:titles][title.title] = title.to_h
    cdh[:titles][title.title][:versions] = title.version_objects.map(&:to_h)

    # the client uses the version_script to determine if a title is installed
    cdh[:titles][title.title][:version_script] = title.version_script_contents if title.version_script

    # add the forced_exclusion_group_name if any
    cdh[:titles][title.title][:excluded_groups] << Xolo::Server.config.forced_exclusion if Xolo::Server.config.forced_exclusion

    # add the frozen group name to the excluded_groups array
    cdh[:titles][title.title][:excluded_groups] << title.jamf_frozen_group_name if title.jamf_frozen_group_name
  end
  # TESTING
  # outfile = Pathname.new('/tmp/client-data.json')
  # outfile.pix_save JSON.pretty_generate(cdh)

  cdh
end

#client_data_jpackageJamf::JPackage

Returns the xolo-client-data package object.

Returns:

  • (Jamf::JPackage)

    the xolo-client-data package object



148
149
150
151
152
153
154
# File 'lib/xolo/server/helpers/client_data.rb', line 148

def client_data_jpackage
  return @client_data_jpackage if @client_data_jpackage

  @client_data_jpackage = Jamf::JPackage.fetch packageName: client_data_package_name, cnx: jamf_cnx
rescue Jamf::NoSuchItemError
  @client_data_jpackage = create_client_data_jamf_package
end

#client_data_manual_policy_nameString

The name of the Jamf::Policy object that installs the xolo-client-data package manually on a managed Mac

Returns:

  • (String)

    the name of the manual policy for the client-data package in Jamf Pro



132
133
134
# File 'lib/xolo/server/helpers/client_data.rb', line 132

def client_data_manual_policy_name
  @client_data_manual_policy_name ||= "#{client_data_package_name}-manual"
end

#client_data_manual_policy_triggerString

The trgger event for the manual policy to update the client data JSON file

Returns:

  • (String)

    the trigger event for the manual policy to update the client data JSON file



139
140
141
# File 'lib/xolo/server/helpers/client_data.rb', line 139

def client_data_manual_policy_trigger
  @client_data_manual_policy_trigger ||= Xolo::Server.config.test_server ? 'test-update-xolo-client-data' : 'update-xolo-client-data'
end

#client_data_package_fileString

The name of the package file that installs the xolo-client-data JSON file

Returns:

  • (String)

    the name of the package file for the client-data package



115
116
117
# File 'lib/xolo/server/helpers/client_data.rb', line 115

def client_data_package_file
  @client_data_package_file ||= "#{client_data_package_name}.pkg"
end

#client_data_package_identifierString

The package identifier for the xolo-client-data package

Returns:

  • (String)

    the package identifier for the xolo-client-data package



100
101
102
# File 'lib/xolo/server/helpers/client_data.rb', line 100

def client_data_package_identifier
  @client_data_package_identifier ||= Xolo::Server.config.test_server ? "com.pixar.xolotest.#{CLIENT_DATA_STR}" : "com.pixar.xolo.#{CLIENT_DATA_STR}"
end

#client_data_package_nameString

The name of the Jamf Package object that contains the xolo client-data NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY

Returns:

  • (String)

    the name of the client-data package in Jamf Pro



108
109
110
# File 'lib/xolo/server/helpers/client_data.rb', line 108

def client_data_package_name
  @client_data_package_name ||= "#{jamf_obj_name_pfx_base}#{CLIENT_DATA_STR}"
end

#client_data_testingObject

temp



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/xolo/server/helpers/client_data.rb', line 418

def client_data_testing
  this_file = Pathname.new(__FILE__).expand_path
  log_debug "this_file: #{this_file}"
  # parent 1 == helpers
  # parent 2 == server
  # parent 3 == xolo
  # parent 4 == lib
  # parent 5 == root
  data_dir = this_file.parent.parent.parent.parent.parent + 'data'
  log_debug "data_dir: #{data_dir}"
  log_debug "data_dir exists? #{data_dir.exist?}"
  log_debug "data_dir children: #{data_dir.children}"
  client_dir = data_dir + 'client'
  log_debug "client_dir: #{client_dir}"
  log_debug "client_dir exists? #{client_dir.exist?}"
  log_debug "client_dir children: #{client_dir.children}"
  client_app = client_dir + 'xolo'
  log_debug "client_app: #{client_app}"
  log_debug "client_app exists? #{client_app.exist?}"
end

#create_client_data_jamf_packageJamf::JPackage

Create and return the xolo-client-data package in Jamf Pro

Returns:

  • (Jamf::JPackage)


196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/xolo/server/helpers/client_data.rb', line 196

def create_client_data_jamf_package
  progress "Jamf: Creating package object '#{client_data_package_name}'"

  info = "Installs the xolo client data JSON file into /Library/Application Support/xolo/#{client_data_filename}"

  # Create the package
  pkg = Jamf::JPackage.create(
    cnx: jamf_cnx,
    packageName: client_data_package_name,
    fileName: client_data_package_file,
    categoryId: jamf_xolo_category_id,
    info: info
  )

  pkg.save
  # .pkg files are not uploaded here, but in the upload_client_data_package method

  log_debug "Jamf: Created package '#{client_data_package_name}'"

  pkg
rescue => e
  raise "Jamf: Error creating Jamf::JPackage '#{client_data_package_name}': #{e.class}: #{e}"
end

#create_client_data_policies_if_neededvoid

This method returns an undefined value.

Create the xolo-client-data policies in Jamf Pro



224
225
226
227
228
229
230
231
232
# File 'lib/xolo/server/helpers/client_data.rb', line 224

def create_client_data_policies_if_needed
  all_pol_names = Jamf::Policy.all_names(cnx: jamf_cnx)

  create_client_data_policy client_data_auto_policy_name unless all_pol_names.include? client_data_auto_policy_name

  return if all_pol_names.include? client_data_manual_policy_name

  create_client_data_policy client_data_manual_policy_name
end

#create_client_data_policy(pol_name) ⇒ void

This method returns an undefined value.

Create a xolo-client-data install policy in Jamf Pro

Parameters:



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
# File 'lib/xolo/server/helpers/client_data.rb', line 240

def create_client_data_policy(pol_name)
  progress "Jamf: Creating policy '#{pol_name}'"

  # Create the policy and set common attributes
  pol = Jamf::Policy.create name: pol_name, cnx: jamf_cnx
  pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
  pol.add_package client_data_package_name

  # scope to all computers
  pol.scope.set_all_targets

  # exclude the forced exclusion group if any
  if valid_forced_exclusion_group_name
    pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name]
    log_info "Jamf: Excluded computer group: #{Xolo::Server.config.forced_exclusion} from policy '#{pol_name}'"
  end

  # Set the trigger event and frequency
  if pol_name == client_data_auto_policy_name
    pol.set_trigger_event :checkin, true
    pol.set_trigger_event :custom, Xolo::BLANK
    pol.frequency = :daily
  elsif pol_name == client_data_manual_policy_name
    pol.set_trigger_event :checkin, false
    pol.set_trigger_event :custom, client_data_manual_policy_name
    pol.frequency = :ongoing
  else
    err_msg = "Jamf: Invalid policy name '#{pol_name}' must be #{client_data_auto_policy_name} or #{client_data_manual_policy_name}"
    log_err err_msg, alert: true
    return
  end
  pol.enable

  pol.save
  log_info "Jamf: Created policy '#{pol_name}'"
end

#create_new_client_data_pkg_filePathname

Create the xolo-client-data package installer file. The xolo client executable is deployed as a separate thing in a Xolo Title

Returns:

  • (Pathname)

    the path to the new package file



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/xolo/server/helpers/client_data.rb', line 292

def create_new_client_data_pkg_file
  pkg_version = Time.now.strftime '%Y%m%d.%H%M%S.%6N'
  work_dir_prefix = "#{client_data_package_name}-#{pkg_version}"

  pkg_work_dir = Pathname.new(Dir.mktmpdir(work_dir_prefix))

  # The client data JSON file
  root_dir = pkg_work_dir + 'pkgroot'
  xolo_client_dir = root_dir + 'Library' + 'Application Support' + 'xolo'
  xolo_client_dir.mkpath
  client_data_file = xolo_client_dir + client_data_filename
  client_data_file.pix_save JSON.pretty_generate(client_data_hash)

  # build the component package
  progress "Jamf: Creating new client-data pkg file '#{client_data_package_file}'", log: :info

  unlock_signing_keychain

  component_pkg_file = build_component_client_data_pkg_file(root_dir, pkg_version, pkg_work_dir)

  build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir)
end

#flush_client_data_policy_logsvoid

This method returns an undefined value.

Flush the logs for the xolo-client-data policies



281
282
283
284
285
# File 'lib/xolo/server/helpers/client_data.rb', line 281

def flush_client_data_policy_logs
  progress "Jamf: Flushing logs for policy #{client_data_auto_policy_name}", log: :info
  pol = Jamf::Policy.fetch name: client_data_auto_policy_name, cnx: jamf_cnx
  pol.flush_logs
end

#update_client_datavoid

This method returns an undefined value.

update the xolo-client-data package and the policy that installs it

This package installs a JSON file with data about all titles and versions for use by the xolo client on managed Macs.

This process is protected by a mutex to prevent multiple updates at the same time.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/xolo/server/helpers/client_data.rb', line 165

def update_client_data
  # don't do anything if we are in developer/test mode
  if Xolo::Server.config.developer_mode?
    log_debug 'Jamf: Skipping client-data update in developer mode'
    return
  end

  log_info 'Jamf: Updating client-data package'

  # TODO: Use Concurrent Ruby instead of Mutex
  mutex = Xolo::Server::Helpers::ClientData.client_data_mutex

  until mutex.try_lock
    progress 'Waiting for another client data update to finish', log: :info
    sleep 5
  end

  new_pkg = create_new_client_data_pkg_file
  upload_to_dist_point client_data_jpackage, new_pkg

  create_client_data_policies_if_needed

  flush_client_data_policy_logs
ensure
  mutex.unlock if mutex&.owned?
end