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_PACKAGE_NAME =

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

"#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{CLIENT_DATA_STR}"
CLIENT_DATA_COMPONENT_PACKAGE_FILE =

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

"#{CLIENT_DATA_PACKAGE_NAME}-component.pkg"
CLIENT_DATA_PACKAGE_FILE =
"#{CLIENT_DATA_PACKAGE_NAME}.pkg"
CLIENT_DATA_PACKAGE_IDENTIFIER =

The package identifier for the xolo-client-data package

"com.pixar.xolo.#{CLIENT_DATA_STR}"
CLIENT_DATA_AUTO_POLICY_NAME =

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

"#{CLIENT_DATA_PACKAGE_NAME}-auto"
CLIENT_DATA_MANUAL_POLICY_NAME =

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

"#{CLIENT_DATA_PACKAGE_NAME}-manual"
CLIENT_DATA_FILE =

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/

'client-data.json'
CLIENT_DATA_MANUAL_POLICY_TRIGGER =

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

'update-xolo-client-data'

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



103
104
105
# File 'lib/xolo/server/helpers/client_data.rb', line 103

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

.extended(extender) ⇒ Object

when this module is extended



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

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

.included(includer) ⇒ Object

when this module is included



88
89
90
# File 'lib/xolo/server/helpers/client_data.rb', line 88

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



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

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



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 323

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



377
378
379
380
381
382
383
384
# File 'lib/xolo/server/helpers/client_data.rb', line 377

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_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



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

def client_data_hash
  cdh = {
    titles: {}
  }
  all_title_objects.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
    if Xolo::Server.config.forced_exclusion
      cdh[:titles][title.title][:excluded_groups] << Xolo::Server.config.forced_exclusion
    end

    # 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



116
117
118
119
120
121
122
# File 'lib/xolo/server/helpers/client_data.rb', line 116

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_testingObject

temp



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/xolo/server/helpers/client_data.rb', line 388

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)


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

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_FILE}"

  # 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 StandardError => 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



192
193
194
195
196
197
198
199
200
201
202
# File 'lib/xolo/server/helpers/client_data.rb', line 192

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

  unless all_pol_names.include? CLIENT_DATA_AUTO_POLICY_NAME
    create_client_data_policy CLIENT_DATA_AUTO_POLICY_NAME
  end

  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:



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

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_TRIGGER
    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



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/xolo/server/helpers/client_data.rb', line 262

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_FILE
  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



251
252
253
254
255
# File 'lib/xolo/server/helpers/client_data.rb', line 251

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.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/xolo/server/helpers/client_data.rb', line 133

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