xolo-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +177 -0
  3. data/README.md +7 -0
  4. data/bin/xoloserver +106 -0
  5. data/data/com.pixar.xoloserver.plist +29 -0
  6. data/data/uninstall-pkgs-by-id.zsh +103 -0
  7. data/lib/xolo/server/app.rb +133 -0
  8. data/lib/xolo/server/command_line.rb +216 -0
  9. data/lib/xolo/server/configuration.rb +739 -0
  10. data/lib/xolo/server/constants.rb +70 -0
  11. data/lib/xolo/server/helpers/auth.rb +257 -0
  12. data/lib/xolo/server/helpers/client_data.rb +415 -0
  13. data/lib/xolo/server/helpers/file_transfers.rb +265 -0
  14. data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
  15. data/lib/xolo/server/helpers/log.rb +97 -0
  16. data/lib/xolo/server/helpers/maintenance.rb +401 -0
  17. data/lib/xolo/server/helpers/notification.rb +145 -0
  18. data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
  19. data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
  20. data/lib/xolo/server/helpers/title_editor.rb +92 -0
  21. data/lib/xolo/server/helpers/titles.rb +145 -0
  22. data/lib/xolo/server/helpers/versions.rb +160 -0
  23. data/lib/xolo/server/log.rb +286 -0
  24. data/lib/xolo/server/mixins/changelog.rb +315 -0
  25. data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
  26. data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
  27. data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
  28. data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
  29. data/lib/xolo/server/object_locks.rb +102 -0
  30. data/lib/xolo/server/routes/auth.rb +89 -0
  31. data/lib/xolo/server/routes/jamf_pro.rb +89 -0
  32. data/lib/xolo/server/routes/maint.rb +174 -0
  33. data/lib/xolo/server/routes/title_editor.rb +71 -0
  34. data/lib/xolo/server/routes/titles.rb +285 -0
  35. data/lib/xolo/server/routes/uploads.rb +93 -0
  36. data/lib/xolo/server/routes/versions.rb +261 -0
  37. data/lib/xolo/server/routes.rb +168 -0
  38. data/lib/xolo/server/title.rb +1143 -0
  39. data/lib/xolo/server/version.rb +902 -0
  40. data/lib/xolo/server.rb +205 -0
  41. data/lib/xolo-server.rb +8 -0
  42. metadata +243 -0
@@ -0,0 +1,1668 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Server
14
+
15
+ module Mixins
16
+
17
+ # This is mixed in to Xolo::Server::Title
18
+ # to define Title-related access to the Jamf Pro server
19
+ #
20
+ module TitleJamfAccess
21
+
22
+ # Constants
23
+ #
24
+ ##############################
25
+ ##############################
26
+
27
+ # Module methods
28
+ #
29
+ # These are available as module methods but not as 'helper'
30
+ # methods in sinatra routes & views.
31
+ #
32
+ ##############################
33
+ ##############################
34
+
35
+ # when this module is included
36
+ ##############################
37
+ def self.included(includer)
38
+ Xolo.verbose_include includer, self
39
+ end
40
+
41
+ # Instance methods
42
+ #
43
+ # These are available directly in title objects
44
+ #
45
+ ##############################
46
+ ##############################
47
+
48
+ ####### The Xolo Title itself
49
+ ###########################################
50
+ ###########################################
51
+
52
+ # @return [String] The start of the Jamf Pro URL for GUI/WebApp access
53
+ ################
54
+ def jamf_gui_url
55
+ server_app_instance.jamf_gui_url
56
+ end
57
+
58
+ # Create title-level things in jamf when creating a title.
59
+ #
60
+ # @return [void]
61
+ ################################
62
+ def create_title_in_jamf
63
+ # ORDER MATTERS
64
+
65
+ # create the normal ea if needed
66
+ configure_jamf_normal_ea if version_script
67
+
68
+ # must happen after the normal ea is created
69
+ configure_jamf_installed_group
70
+
71
+ if uninstall_script || !uninstall_ids.pix_empty?
72
+ configure_jamf_uninstall_script
73
+ # this creates the policy to use the script
74
+ # must happen after the uninstall script is created
75
+ jamf_uninstall_policy
76
+
77
+ # this creates the expire policy if needed
78
+ jamf_expire_policy if expiration && !expire_paths.pix_empty?
79
+ end
80
+
81
+ # Create the static group that will contain computers where this title is 'frozen'
82
+ # Just calling this will create it if it doesn't exist.
83
+ jamf_frozen_group
84
+
85
+ activate_jamf_patch_title
86
+ end
87
+
88
+ # Apply any changes to Jamf as needed
89
+ # Mostly this just sets flags indicating what needs to be updated in the
90
+ # various version-related things in jamf - policies, self service, etc.
91
+ #
92
+ # @return [void]
93
+ #########################
94
+ def update_title_in_jamf
95
+ # ORDER MATTERS
96
+
97
+ # do we have a version_script? if so we maintain a 'normal' EA
98
+ # this has to happen before updating the installed_group
99
+ configure_jamf_normal_ea if need_to_update_jamf_normal_ea?
100
+
101
+ # this smart group might use the normal-EA or might use app data
102
+ # If those have changed, we need to update it.
103
+ configure_jamf_installed_group if need_to_update_jamf_installed_group?
104
+
105
+ # if the exclusions have changed update the manual install released policy
106
+ if changes_for_update[:excluded_groups]
107
+ progress "Jamf: Updating excluded groups for Manual Released Policy '#{jamf_manual_install_released_policy_name}'."
108
+ configure_jamf_manual_install_released_policy(jamf_manual_install_released_policy)
109
+ end
110
+
111
+ # Do we need to update (vs delete) the uninstall script?
112
+ if need_to_update_jamf_uninstall_script?
113
+
114
+ configure_jamf_uninstall_script
115
+ configure_jamf_uninstall_policy
116
+
117
+ # this creates the policy to use the script, if needed
118
+ # which needs both the uninstall script and the installed group to exist
119
+ jamf_uninstall_policy
120
+
121
+ # or delete it if no longer needed
122
+ elsif need_to_delete_jamf_uninstall_script?
123
+ delete_jamf_uninstall_policy
124
+ delete_jamf_uninstall_script
125
+ # can't expire without
126
+ end
127
+
128
+ # Do we need to add or delete the expire policy?
129
+ # NOTE: if the uninstall script was deleted,
130
+ # expiration won't do anything.
131
+ if need_to_update_expiration?
132
+ changes_for_update.dig(:expiration, :new).to_i.positive? ? jamf_expire_policy : delete_jamf_expire_policy
133
+ end
134
+
135
+ # If we don't use a version script anymore, delete the normal EA
136
+ # this has to happen after updating the installed_group
137
+ delete_jamf_normal_ea unless version_script_contents
138
+
139
+ update_description_in_jamf
140
+ update_ssvc
141
+ update_ssvc_category
142
+ # TODO: deal with icon changes: if changes_for_update&.key? :self_service_icon
143
+
144
+ if jamf_ted_title_active?
145
+ update_versions_for_title_changes_in_jamf
146
+ else
147
+ log_debug "Jamf: Title '#{display_name}' (#{title}) is not yet active to Jamf, nothing to update in versions."
148
+ end
149
+ end
150
+
151
+ # Repair this title in Jamf Pro
152
+ # - TODO: activate title in patch mgmt
153
+ # - TODO: Accept Patch EA
154
+ # - Normal EA 'xolo-<title>-installed-version'
155
+ # - title-installed smart group 'xolo-<title>-installed'
156
+ # - frozen static group 'xolo-<title>-frozen'
157
+ # - manual/SSvc install-current-release policy 'xolo-<title>-install'
158
+ # - trigger 'xolo-<title>-install'
159
+ # - ssvc icon
160
+ # - ssvc category
161
+ # - description
162
+ # - if uninstallable
163
+ # - uninstall script 'xolo-<title>-uninstall'
164
+ # - uninstall policy 'xolo-<title>-uninstall'
165
+ # - if expirable
166
+ # - expire policy 'xolo-<title>-expire'
167
+ # - trigger 'xolo-<title>-expire'
168
+ #
169
+ ###############################################
170
+ def repair_jamf_title_objects
171
+ progress "Jamf: Repairing Jamf objects for title '#{title}'", log: :info
172
+ repair_jamf_normal_ea
173
+ configure_jamf_installed_group
174
+ repair_jamf_uninstall_policy
175
+ repair_jamf_uninstall_script
176
+ repair_jamf_expire_policy
177
+ repair_frozen_group
178
+ repair_jamf_manual_install_released_policy
179
+ end
180
+
181
+ # Delete an entire title from Jamf Pro
182
+ # alway delete policies first, then scripts, then groups, then EAs, then the patch title
183
+ ########################
184
+ def delete_title_from_jamf
185
+ # ORDER MATTERS
186
+ delete_jamf_expire_policy
187
+ delete_jamf_uninstall_policy
188
+ delete_jamf_manual_install_released_policy
189
+ delete_jamf_uninstall_script
190
+ delete_jamf_frozen_group
191
+ delete_jamf_installed_group
192
+ delete_jamf_normal_ea
193
+ delete_jamf_patch_title
194
+ end
195
+
196
+ # If any title changes require updates to existing versions in
197
+ # Jamf, this loops thru the versions and applies
198
+ # them
199
+ #
200
+ # This should happen after the incoming changes have been applied to this
201
+ # title instance
202
+ #
203
+ # Jamf Stuff
204
+ # - update any policy scopes
205
+ # - update any policy SSvc settings
206
+ #
207
+ # @return [void]
208
+ ############################
209
+ def update_versions_for_title_changes_in_jamf
210
+ version_objects.each do |vers_obj|
211
+ vers_obj.update_release_groups(ttl_obj: self) if changes_for_update&.key? :release_groups
212
+ vers_obj.update_excluded_groups(ttl_obj: self) if changes_for_update&.key? :excluded_groups
213
+ vers_obj.update_jamf_package_notes(ttl_obj: self) if need_to_update_description?
214
+ # vers_obj.update_ssvc(ttl_obj: self) if changes_for_update&.key? :self_service
215
+ # vers_obj.update_ssvc_category(ttl_obj: self) if changes_for_update&.key? :self_service_category
216
+ end
217
+ end
218
+
219
+ # Update the description in Jamfy places it appears
220
+ # At the moment, this is only the manual install policy
221
+ # if its in self service. The package notes are updated
222
+ # by the versions themselves via the
223
+ # update_versions_for_title_changes_in_jamf method
224
+ #
225
+ # @return [void]
226
+ #########################
227
+ def update_description_in_jamf
228
+ return unless need_to_update_description?
229
+
230
+ # Update the manual install policy
231
+ return unless self_service
232
+
233
+ pol = jamf_manual_install_released_policy
234
+ return unless pol
235
+
236
+ progress "Jamf: Updating Description for Self Service in policy '#{pol.name}'.", log: :info
237
+ new_desc = changes_for_update[:description][:new] || description
238
+ pol.self_service_description = new_desc
239
+ pol.save
240
+ end
241
+
242
+ # do we need to update the description?
243
+ # True if our incoming changes include :description
244
+ #
245
+ # @return [Boolean]
246
+ ###################################
247
+ def need_to_update_description?
248
+ changes_for_update.key?(:description)
249
+ end
250
+
251
+ # do we need to create or delete the expire policy?
252
+ # True if our incoming changes include :expiration
253
+ #
254
+ # Ignore the expire paths - even when disabling expiration
255
+ # they can stay there, they won't mean anything.
256
+ #
257
+ # @return [Boolean]
258
+ ###################################
259
+ def need_to_update_expiration?
260
+ changes_for_update.key?(:expiration)
261
+ end
262
+
263
+ # Get the patch report for this title.
264
+ # It's the JPAPI report data with each hash having a frozen: key added
265
+ #
266
+ # TODO: rework this when all the paging stuff is handled by ruby-jss
267
+ #
268
+ # @param vers [String, nil] Limit the report to a specific version
269
+ #
270
+ # @return [Arrah<Hash>] Data for each computer with any version of this title installed
271
+ ######################
272
+ def patch_report(vers: nil)
273
+ vers = Xolo::Server::Helpers::JamfPro::PATCH_REPORT_UNKNOWN_VERSION if vers == Xolo::UNKNOWN
274
+ vers &&= CGI.escape vers.to_s
275
+
276
+ page_size = Xolo::Server::Helpers::JamfPro::PATCH_REPORT_JPAPI_PAGE_SIZE
277
+ page = 0
278
+ paged_rsrc = "#{patch_report_rsrc}?page=#{page}&page-size=#{page_size}"
279
+ paged_rsrc << "&filter=version%3D%3D#{vers}" if vers
280
+
281
+ report = []
282
+ loop do
283
+ data = jamf_cnx.jp_get(paged_rsrc)[:results]
284
+ log_debug "GOT #{paged_rsrc} >>> results size: #{data.size}"
285
+ break if data.empty?
286
+
287
+ report += data
288
+ page += 1
289
+ paged_rsrc = "#{patch_report_rsrc}?page=#{page}&page-size=#{page_size}"
290
+ paged_rsrc << "&filter=version%3D%3D#{vers}" if vers
291
+ end
292
+
293
+ # log_debug "REPORT: #{report}"
294
+
295
+ frozen_comps = frozen_computers.keys
296
+ report.each do |h|
297
+ h[:frozen] = frozen_comps.include? h[:computerName]
298
+ h[:version] = Xolo::UNKNOWN if h[:version] == Xolo::Server::Helpers::JamfPro::PATCH_REPORT_UNKNOWN_VERSION
299
+ end
300
+ report
301
+ end
302
+
303
+ ####### The'Normal" Extension Attribute
304
+ ###########################################
305
+ ###########################################
306
+
307
+ # @return [Boolean] Does the 'normal' EA exist in jamf?
308
+ #########################
309
+ def jamf_normal_ea_exist?
310
+ Jamf::ComputerExtensionAttribute.all_names(:refresh, cnx: jamf_cnx).include? jamf_normal_ea_name
311
+ end
312
+
313
+ # Create or fetch the 'normal' EA in jamf
314
+ # If we are deleting and it doesn't exist, return nil.
315
+ #
316
+ # @return [Jamf::ComputerExtensionAttribute] The 'normal' Jamf ComputerExtensionAttribute for this title
317
+ ########################
318
+ def jamf_normal_ea
319
+ return @jamf_normal_ea if @jamf_normal_ea
320
+
321
+ if jamf_normal_ea_exist?
322
+ @jamf_normal_ea = Jamf::ComputerExtensionAttribute.fetch(name: jamf_normal_ea_name, cnx: jamf_cnx)
323
+
324
+ else
325
+ return if deleting?
326
+
327
+ msg = "Jamf: Creating regular extension attribute '#{jamf_normal_ea_name}' for use in smart groups"
328
+ progress msg, log: :info
329
+
330
+ @jamf_normal_ea = Jamf::ComputerExtensionAttribute.create(
331
+ name: jamf_normal_ea_name,
332
+ cnx: jamf_cnx
333
+ )
334
+ @jamf_normal_ea.save
335
+
336
+ end
337
+ @jamf_normal_ea
338
+ end
339
+
340
+ # Configure the 'normal' EA that matches the Patch EA for this title,
341
+ # so that it can be used in smart groups and adv. searches.
342
+ # (Patch EAs aren't available for use in smart group critera)
343
+ #
344
+ # If we have one already but are deleting it, that happens elsewhere
345
+ #
346
+ # @return [void]
347
+ ################################
348
+ def configure_jamf_normal_ea
349
+ progress "Jamf: Configuring regular extension attribute '#{jamf_normal_ea_name}'", log: :info
350
+
351
+ jamf_normal_ea.description = "The version of xolo title '#{title}' installed on the machine"
352
+ jamf_normal_ea.data_type = :string
353
+
354
+ # this is our incoming or already-existing EA script
355
+ if version_script_contents.pix_empty?
356
+ # nothing to do if its nil, if we need to delete it, that'll happen later
357
+ else
358
+ jamf_normal_ea.enabled = true
359
+ jamf_normal_ea.input_type = 'script'
360
+ jamf_normal_ea.script = version_script_contents
361
+ end
362
+
363
+ jamf_normal_ea.script = scr
364
+ jamf_normal_ea.save
365
+ end
366
+
367
+ # Repair the 'normal' EA in jamf to match our version_script
368
+ ########################
369
+ def repair_jamf_normal_ea
370
+ if version_script_contents.pix_empty?
371
+ delete_jamf_normal_ea
372
+ else
373
+ configure_jamf_normal_ea
374
+ end
375
+ end
376
+
377
+ # Delete the 'normal' computer ext attr matching the Patch EA
378
+ # @return [void]
379
+ ######################################
380
+ def delete_jamf_normal_ea
381
+ ea_id = Jamf::ComputerExtensionAttribute.valid_id jamf_normal_ea_name, cnx: jamf_cnx
382
+ return unless ea_id
383
+
384
+ progress "Jamf: Deleting regular extension attribute '#{jamf_normal_ea_name}'", log: :info
385
+ Jamf::ComputerExtensionAttribute.delete ea_id, cnx: jamf_cnx
386
+ end
387
+
388
+ # do we need to update the normal EA in jamf?
389
+ # true if our incoming changes include :version_script
390
+ # and the new value is not empty (in which case we'll delete it)
391
+ #
392
+ # @return [Boolean]
393
+ ########################
394
+ def need_to_update_jamf_normal_ea?
395
+ changes_for_update.key?(:version_script) && !changes_for_update[:version_script][:new].pix_empty?
396
+ end
397
+
398
+ # the script contents of the Normal Jamf EA that comes from our version_script
399
+ # @return [String, nil] nil if there is none
400
+ ##############################
401
+ def jamf_normal_ea_contents
402
+ return unless jamf_normal_ea_exist?
403
+
404
+ jamf_normal_ea.script
405
+ end
406
+
407
+ # @return [String] the URL for the Normal EA in Jamf Pro
408
+ ######################
409
+ def jamf_normal_ea_url
410
+ return @jamf_normal_ea_url if @jamf_normal_ea_url
411
+ return unless version_script
412
+
413
+ ea_id = Jamf::ComputerExtensionAttribute.valid_id jamf_normal_ea_name, cnx: jamf_cnx
414
+ return unless ea_id
415
+
416
+ # Jamf Changed the URL!
417
+ # @jamf_normal_ea_url = "#{jamf_gui_url}/computerExtensionAttributes.html?id=#{ea_id}&o=r"
418
+
419
+ @jamf_normal_ea_url = "#{jamf_gui_url}/view/settings/computer-management/computer-extension-attributes/#{ea_id}"
420
+ end
421
+
422
+ ####### The Patch Ext Attribute
423
+ ###########################################
424
+ ###########################################
425
+
426
+ # Do we need to accept the patch ea in jamf?
427
+ #
428
+ # True if
429
+ # - title activation, and version_script
430
+ # - any time version_script changes
431
+ # - any time we switch from bundle data to version_script
432
+ #
433
+ # @need_to_accept_jamf_patch_ea is set true by these methods
434
+ # - Title#create_ted_ea
435
+ # - Title#update_ted_ea
436
+ #
437
+ # @return [Boolean]
438
+ #########################
439
+ def need_to_accept_jamf_patch_ea?
440
+ @need_to_accept_jamf_patch_ea
441
+ end
442
+
443
+ # This method should only be called when we *expect* to need to accept the EA -
444
+ # not only when we first activate a title with a version script, but when the version_script
445
+ # has changed, or been added, replacing app_name and app_bundle_id.
446
+ #
447
+ # If the EA needs acceptance when this method starts, we accept it and we're done.
448
+ #
449
+ # If not (there is no EA, or it's already accepted) then we spin off a thread that
450
+ # waits up to an hour for Jamf to notice the change from the Title Editor and require
451
+ # re-acceptance.
452
+ #
453
+ # As soon as we see that Jamf shows accepted: false, we'll accept it and be done.
454
+ #
455
+ # If we make it for an hour and never see the expected need for acceptance, we
456
+ # log it and send an alert about it.
457
+ #
458
+ # TODO: when this is implemented in ruby-jss, use the implementation
459
+ #
460
+ # NOTE: PATCHing the ea of the title requires CRUD privs for computer ext attrs
461
+ #
462
+ # @return [void]
463
+ ############################
464
+ def accept_jamf_patch_ea
465
+ return unless need_to_accept_jamf_patch_ea?
466
+
467
+ # return with warning if we aren't auto-accepting
468
+ unless Xolo::Server.config.jamf_auto_accept_xolo_eas
469
+ msg = "Jamf: IMPORTANT: Before adding any versions, the Extension Attribute (version-script) for this title must be accepted manually in Jamf Pro at #{jamf_patch_ea_url} under the 'Extension Attribute' tab (click 'Edit') "
470
+ progress msg
471
+ log_debug 'Admin informed about accepting EA/version-script manually'
472
+ return
473
+ end
474
+
475
+ # this is true if the Jamf server already knows it needs to be accepted
476
+ # so just do it now
477
+ if jamf_patch_ea_awaiting_acceptance?
478
+ progress "Jamf: Auto-accepting use of version-script ExtensionAttribute '#{ted_ea_key}'", log: :info
479
+ accept_jamf_patch_ea_via_api
480
+ return
481
+ end
482
+
483
+ # If not, we are here because we expect it will need acceptance soon
484
+ # So we call this method to wait for Jamf to notice that,
485
+ # checking in the background for up to an hour.
486
+ auto_accept_patch_ea_in_thread
487
+ end
488
+
489
+ # Wait for up to an hour for Jamf to notice that our TEd EA needs to be
490
+ # accepted, and then do it
491
+ #####################
492
+ def auto_accept_patch_ea_in_thread
493
+ # don't do this if there's already one running for this instance
494
+ if @auto_accept_ea_thread&.alive?
495
+ log_debug "Jamf: auto_accept_ea_thread already running. Caller: #{caller_locations.first}"
496
+ return
497
+ end
498
+
499
+ progress "Jamf: version-script ExtAttr for this title '#{ted_ea_key}' will be auto-accepted when Jamf sees the changes in the Title Editor"
500
+
501
+ @auto_accept_ea_thread = Thread.new do
502
+ log_debug "Jamf: Starting auto_accept_ea_thread for #{title}"
503
+ start_time = Time.now
504
+ max_time = start_time + Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR
505
+
506
+ start_time = start_time.strftime '%F %T'
507
+ did_it = false
508
+
509
+ while Time.now < max_time
510
+ sleep 30
511
+
512
+ # refresh our jamf connection cuz it might expire if this takes a while, esp if using
513
+ # an APIClient
514
+ jamf_cnx(refresh: true) if jamf_cnx.token.secs_remaining < 90
515
+
516
+ log_debug "Jamf: checking for expected (re)acceptance of version-script ExtensionAttribute '#{ted_ea_key}' since #{start_time}"
517
+ next unless jamf_patch_ea_awaiting_acceptance?
518
+
519
+ accept_jamf_patch_ea_via_api
520
+ log_info "Jamf: Auto-accepted use of version-script ExtensionAttribute '#{ted_ea_key}'"
521
+ did_it = true
522
+ break
523
+ end # while
524
+
525
+ unless did_it
526
+ msg = "Jamf: Expected to (re)accept version-script ExtensionAttribute '#{ted_ea_key}', but Jamf hasn't seen the change in over #{Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR} secs. Please investigate."
527
+ log_error msg, alert: true
528
+ end
529
+ end # thread
530
+ @auto_accept_ea_thread.name = "auto_accept_ea_thread for #{title}"
531
+ end
532
+
533
+ # Does the Jamf Title currently need its EA to be accepted, according to Jamf Pro?
534
+ #
535
+ # NOTE: Jamf might not see the need for this immediately, so we set
536
+ # @need_to_accept_jamf_patch_ea and define #need_to_accept_jamf_patch_ea?
537
+ # and use them to determine if we should wait for this to become true.
538
+ #
539
+ # @return [Boolean]
540
+ #################################
541
+ def jamf_patch_ea_awaiting_acceptance?
542
+ ead = jamf_patch_ea_data
543
+ return unless ead
544
+
545
+ !ead[:accepted]
546
+ end
547
+
548
+ # Does the EA for this title in Jamf match the version script we know about?
549
+ #
550
+ # If we don't have a version script, then we don't really care what Jamf has at the moment,
551
+ # Jamf's should go away once it catches up with the title editor.
552
+ #
553
+ # But if we do have one, and Jamf has something different, we'll need to accept it,
554
+ # if configured to do so automatically.
555
+ #
556
+ # This method just tells us the current situation about our version script
557
+ # vs the Jamf Patch EA.
558
+ #
559
+ # @param new_version_script [String, nil] If updating, this is the new incoming version script.
560
+ #
561
+ # @return [Boolean, nil] nil if we have no version script,
562
+ # otherwise, does jamf match our version_script?
563
+ #########################
564
+ def jamf_patch_ea_matches_version_script?
565
+ # our current version script - nil if we currently don't have one
566
+ our_version_script = version_script_contents
567
+
568
+ # we don't have one, so if Jamf does at the moment, it'll go away soon
569
+ # when jamf catches up with the title editor.
570
+ return unless our_version_script
571
+
572
+ # does jamf's script match ours?
573
+ our_version_script.chomp == jamf_patch_ea_contents.chomp
574
+ end
575
+
576
+ # The version_script as a Jamf Extension Attribute,
577
+ # once the title as been activated in Jamf.
578
+ #
579
+ # This is a hash of data returned from the JP API endpoint:
580
+ # "v2/patch-software-title-configurations/#{jamf_patch_title_id}/extension-attributes"
581
+ # which has these keys:
582
+ #
583
+ # :accepted [Boolean] has it been accepted for the title?
584
+ #
585
+ # :eaId [String] the 'key' of the EA from the title editor
586
+ #
587
+ # :displayName [String] the displayname from the title editor, for titles
588
+ # maintained by xolo, it's the same as the eaId
589
+ #
590
+ # :scriptContent [String] the Base64-encoded script of the EA.
591
+ #
592
+ # TODO: when this gets implemented in ruby-jss, use that implementation
593
+ # and return the patch title ea object.
594
+ #
595
+ # NOTE: The title must be activated in Jamf before accessing this.
596
+ #
597
+ # NOTE: We fetch this hash every time this method is called, since we may
598
+ # be waiting for jamf to notice that the EA has changed in the Title Editor
599
+ # and needs re-acceptance
600
+ #
601
+ # NOTE: While Jamf Patch allows for multiple EAs per title, the Title Editor only
602
+ # allows for one. So even tho the data comes back in an array, we only care about
603
+ # the first (and only) value.
604
+ #
605
+ # @return [Hash] the data from the JPAPI endpoint,
606
+ # nil if the title has no EA at the moment
607
+ ########################
608
+ def jamf_patch_ea_data
609
+ return unless jamf_patch_title_id
610
+
611
+ jamf_cnx.jp_get("v2/patch-software-title-configurations/#{jamf_patch_title_id}/extension-attributes").first
612
+ end
613
+
614
+ # API call to accept the version-script EA in Jamf Pro
615
+ # TODO: when this gets implemented in ruby-jss, use that implementation
616
+ #
617
+ # @return [void]
618
+ ########################
619
+ def accept_jamf_patch_ea_via_api
620
+ patchdata = <<~ENDPATCHDATA
621
+ {
622
+ "extensionAttributes": [
623
+ {
624
+ "accepted": true,
625
+ "eaId": "#{ted_ea_key}"
626
+ }
627
+ ]
628
+ }
629
+ ENDPATCHDATA
630
+ jamf_cnx.jp_patch "v2/patch-software-title-configurations/#{jamf_patch_title_id}", patchdata
631
+ log_debug "Jamf: Auto-accepted ExtensionAttribute '#{ted_ea_key}'"
632
+ end
633
+
634
+ # the script contents of the Jamf Patch EA that comes from our version_script
635
+ # @return [String, nil] nil if there is none, or the title isn't active yet
636
+ ##############################
637
+ def jamf_patch_ea_contents
638
+ jea_data = jamf_patch_ea_data
639
+ return unless jea_data && jea_data[:scriptContents]
640
+
641
+ Base64.decode64 jea_data[:scriptContents]
642
+ end
643
+
644
+ # @return [String] the URL for the Patch EA in Jamf Pro
645
+ ######################
646
+ def jamf_patch_ea_url
647
+ return @jamf_patch_ea_url if @jamf_patch_ea_url
648
+ return unless version_script
649
+
650
+ @jamf_patch_ea_url = "#{jamf_patch_title_url}?tab=extension"
651
+ end
652
+
653
+ ####### The UnInstall Script
654
+ ###########################################
655
+ ###########################################
656
+
657
+ # @return [Boolean] Does the uninstall script exist in jamf?
658
+ ##########################
659
+ def jamf_uninstall_script_exist?
660
+ Jamf::Script.all_names(:refresh, cnx: jamf_cnx).include? jamf_uninstall_script_name
661
+ end
662
+
663
+ # Create or fetch the script that uninstalls this title from a Mac
664
+ #
665
+ # @return [Jamf::Script] The Jamf Script for uninstalling this title
666
+ #####################################
667
+ def jamf_uninstall_script
668
+ return @jamf_uninstall_script if @jamf_uninstall_script
669
+
670
+ if jamf_uninstall_script_exist?
671
+ @jamf_uninstall_script = Jamf::Script.fetch name: jamf_uninstall_script_name, cnx: jamf_cnx
672
+ else
673
+ return if deleting?
674
+
675
+ progress "Jamf: Creating Uninstall script '#{jamf_uninstall_script_name}'", log: :info
676
+ @jamf_uninstall_script = Jamf::Script.create(
677
+ name: jamf_uninstall_script_name,
678
+ cnx: jamf_cnx
679
+ )
680
+ @jamf_uninstall_script.save
681
+ end
682
+ @jamf_uninstall_script
683
+ end
684
+
685
+ # Configure the uninstall script in jamf with our uninstall_script contents
686
+ #
687
+ # @return [void]
688
+ ################################
689
+ def configure_jamf_uninstall_script
690
+ # if we don't have an uninstall script, nothing to do, it will be deleted elsewhere
691
+ return unless uninstall_script_contents
692
+
693
+ progress "Jamf: Congfiguring the uninstall script '#{jamf_uninstall_script_name}'", log: :info
694
+ jamf_uninstall_script.code = uninstall_script_contents
695
+ jamf_uninstall_script.save
696
+ end
697
+
698
+ # do we need to update the uninstall scriptin jamf?
699
+ # true if our incoming changes include :uninstall_script OR :uninstall_ids
700
+ # and the new value of at least one of them is not empty
701
+ #
702
+ # (in which case we'll delete it)
703
+ #
704
+ # @return [Boolean]
705
+ ########################
706
+ def need_to_update_jamf_uninstall_script?
707
+ if changes_for_update.key?(:uninstall_script)
708
+ !changes_for_update[:uninstall_script][:new].pix_empty?
709
+ elsif changes_for_update.key?(:uninstall_ids)
710
+ !changes_for_update[:uninstall_ids][:new].pix_empty?
711
+ else
712
+ false
713
+ end
714
+ end
715
+
716
+ #########################
717
+ def delete_jamf_uninstall_script
718
+ return unless jamf_uninstall_script_exist?
719
+
720
+ progress "Jamf: Deleting uninstall script '#{jamf_uninstall_script_name}'", log: :info
721
+ jamf_uninstall_script.delete
722
+ end
723
+
724
+ # repair the uninstall script and policy in jamf
725
+ #####################
726
+ def repair_jamf_uninstall_script
727
+ if uninstall_script_contents
728
+ configure_jamf_uninstall_script
729
+ else
730
+ delete_jamf_uninstall_script
731
+ end
732
+ end
733
+
734
+ # do we need to delete the uninstall script stuff in jamf?
735
+ #
736
+ # @return [Boolean]
737
+ #############################
738
+ def need_to_delete_jamf_uninstall_script?
739
+ uninstall_script_contents.pix_empty?
740
+ end
741
+
742
+ # @return [String] the URL for the uninstall script in Jamf Pro
743
+ ######################
744
+ def jamf_uninstall_script_url
745
+ return @jamf_uninstall_script_url if @jamf_uninstall_script_url
746
+ return unless uninstallable?
747
+
748
+ scr_id = Jamf::Script.valid_id jamf_uninstall_script_name, cnx: jamf_cnx
749
+ return unless scr_id
750
+
751
+ @jamf_uninstall_script_url = "#{jamf_gui_url}/view/settings/computer-management/scripts/#{scr_id}?tab=script"
752
+ end
753
+
754
+ ####### The Uninstall Policy-
755
+ ###########################################
756
+ ###########################################
757
+
758
+ # @return [Boolean] Does the jamf_uninstall_policy exist?
759
+ #########################
760
+ def jamf_uninstall_policy_exist?
761
+ Jamf::Policy.all_names(:refresh, cnx: jamf_cnx).include? jamf_uninstall_policy_name
762
+ end
763
+
764
+ # Create or fetch the policy that runs the jamf uninstall script
765
+ #
766
+ # @return [Jamf::Policy] The Jamf Policy for uninstalling this title
767
+ #####################################
768
+ def jamf_uninstall_policy
769
+ return @jamf_uninstall_policy if @jamf_uninstall_policy
770
+
771
+ if jamf_uninstall_policy_exist?
772
+ @jamf_uninstall_policy = Jamf::Policy.fetch name: jamf_uninstall_policy_name, cnx: jamf_cnx
773
+ else
774
+ return if deleting?
775
+
776
+ progress "Jamf: Creating Uninstall policy: '#{jamf_uninstall_policy_name}'", log: :info
777
+ @jamf_uninstall_policy = Jamf::Policy.create name: jamf_uninstall_policy_name, cnx: jamf_cnx
778
+ configure_jamf_uninstall_policy(jamf_uninstall_policy)
779
+ end
780
+
781
+ @jamf_uninstall_policy
782
+ end
783
+
784
+ # Configure the uninstall policy
785
+ # @param pol [Jamf::Policy] the policy to configure
786
+ ############################
787
+ def configure_jamf_uninstall_policy(pol = nil)
788
+ progress "Jamf: Configuring uninstall policy '#{jamf_uninstall_policy_name}'", log: :info
789
+ pol ||= jamf_uninstall_policy
790
+ pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
791
+ pol.add_script jamf_uninstall_script_name
792
+ pol.set_trigger_event :checkin, false
793
+ pol.set_trigger_event :custom, jamf_uninstall_policy_name
794
+ pol.scope.add_target(:computer_group, jamf_installed_group_name)
795
+ pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name] if valid_forced_exclusion_group_name
796
+ pol.frequency = :ongoing
797
+ pol.enable
798
+ pol.save
799
+ end
800
+
801
+ # repair the uninstall script and policy in jamf
802
+ #####################
803
+ def repair_jamf_uninstall_policy
804
+ if uninstall_script_contents
805
+ configure_jamf_uninstall_policy
806
+ else
807
+ delete_jamf_uninstall_policy
808
+ end
809
+ end
810
+
811
+ # delete the policy first if it exists
812
+
813
+ #########################
814
+ def delete_jamf_uninstall_policy
815
+ return unless jamf_uninstall_policy_exist?
816
+
817
+ progress "Jamf: Deleting uninstall policy '#{jamf_uninstall_policy_name}'", log: :info
818
+ jamf_uninstall_policy.delete
819
+ end
820
+
821
+ # @return [String] the URL for the uninstall policy in Jamf Pro
822
+ ######################
823
+ def jamf_uninstall_policy_url
824
+ return @jamf_uninstall_policy_url if @jamf_uninstall_policy_url
825
+ return unless uninstallable?
826
+
827
+ pol_id = Jamf::Policy.valid_id jamf_uninstall_policy_name, cnx: jamf_cnx
828
+ return unless pol_id
829
+
830
+ @jamf_uninstall_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
831
+ end
832
+
833
+ ####### The Installed Group
834
+ ###########################################
835
+ ###########################################
836
+
837
+ # @return [Boolean] Does the jamf_installed_group exist?
838
+ ##########################
839
+ def jamf_installed_group_exist?
840
+ Jamf::ComputerGroup.all_names(:refresh, cnx: jamf_cnx).include? jamf_installed_group_name
841
+ end
842
+
843
+ # Create or fetch he smartgroup in jamf that contains all macs
844
+ # with any version of this title installed.
845
+ # If we are deleting and it doesn't exist, return nil.
846
+ #
847
+ # @return [Jamf::ComputerGroup, nil] The Jamf ComputerGroup for this title's installed computers
848
+ #####################################
849
+ def jamf_installed_group
850
+ return @jamf_installed_group if @jamf_installed_group
851
+
852
+ if jamf_installed_group_exist?
853
+ @jamf_installed_group = Jamf::ComputerGroup.fetch(
854
+ name: jamf_installed_group_name,
855
+ cnx: jamf_cnx
856
+ )
857
+ else
858
+ return if deleting?
859
+
860
+ progress "Jamf: Creating smart group '#{jamf_installed_group_name}'", log: :info
861
+
862
+ @jamf_installed_group = Jamf::ComputerGroup.create(
863
+ name: jamf_installed_group_name,
864
+ type: :smart,
865
+ cnx: jamf_cnx
866
+ )
867
+ @jamf_installed_group.save
868
+ configure_jamf_installed_group
869
+ log_debug 'Jamf: Sleeping to let Jamf server see change to the Installed smart group.'
870
+ sleep 10
871
+ end
872
+ @jamf_installed_group
873
+ end
874
+
875
+ # Set the configuration of jamf_installed_group
876
+ #
877
+ # @return [void]
878
+ #####################################
879
+ def configure_jamf_installed_group
880
+ progress "Jamf: Configuring smart group '#{jamf_installed_group_name}'", log: :info
881
+
882
+ jamf_installed_group.criteria = Jamf::Criteriable::Criteria.new(jamf_installed_group_criteria)
883
+ jamf_installed_group.save
884
+ end
885
+
886
+ # The criteria for the smart group in Jamf that contains all Macs
887
+ # with any version of this title installed
888
+ #
889
+ # If we have, or are about to update to, a version_script (EA) then use it,
890
+ # otherwise use the app_name and app_bundle_id.
891
+ #
892
+ # @return [Array<Jamf::Criteriable::Criterion>]
893
+ ###################################
894
+ def jamf_installed_group_criteria
895
+ have_vers_script =
896
+ if changes_for_update&.dig :version_script
897
+ changes_for_update[:version_script][:new]
898
+ else
899
+ version_script
900
+ end
901
+
902
+ # If we have a version_script, use the ea
903
+ if have_vers_script
904
+ [
905
+ Jamf::Criteriable::Criterion.new(
906
+ and_or: :and,
907
+ name: jamf_normal_ea_name,
908
+ search_type: 'is not',
909
+ value: Xolo::BLANK
910
+ )
911
+ ]
912
+
913
+ # No version script, so we must be using app data
914
+ else
915
+ aname = changes_for_update.dig(:app_name, :new) || app_name
916
+ abundle = changes_for_update.dig(:app_bundle_id, :new) || app_bundle_id
917
+
918
+ [
919
+ Jamf::Criteriable::Criterion.new(
920
+ and_or: :and,
921
+ name: 'Application Title',
922
+ search_type: 'is',
923
+ value: aname
924
+ ),
925
+
926
+ Jamf::Criteriable::Criterion.new(
927
+ and_or: :and,
928
+ name: 'Application Bundle ID',
929
+ search_type: 'is',
930
+ value: abundle
931
+ )
932
+ ]
933
+ end
934
+ end
935
+
936
+ # do we need to update the 'installed' smart group?
937
+ # true if our incoming changes include the app_name or app_bundle_id
938
+ #
939
+ # If they changed at all, we need to update no matter what:
940
+ # - if they are now nil, we switched to a version script
941
+ #
942
+ # - if they aren't nil but are different, we need to update
943
+ # the group criteria to reflect that.
944
+ #
945
+ # Changes to the version script, if it was in use before, don't
946
+ # require us to change the smart group
947
+ #
948
+ #
949
+ # @return [Boolean]
950
+ #########################
951
+ def need_to_update_jamf_installed_group?
952
+ changes_for_update[:app_name] || changes_for_update[:app_bundle_id]
953
+ end
954
+
955
+ # Delete the 'installed' smart group
956
+ # @return [void]
957
+ ######################################
958
+ def delete_jamf_installed_group
959
+ return unless jamf_installed_group_exist?
960
+
961
+ progress "Jamf: Deleting smart group '#{jamf_installed_group_name}'", log: :info
962
+ jamf_installed_group.delete
963
+ # give the server time to see the deletion
964
+ log_debug 'Sleeping to let server see deletion of smart group'
965
+ sleep 10
966
+ end
967
+
968
+ # @return [String] the URL for the Frozen statig group in Jamf Pro
969
+ ######################
970
+ def jamf_installed_group_url
971
+ return @jamf_installed_group_url if @jamf_installed_group_url
972
+
973
+ gr_id = Jamf::ComputerGroup.valid_id jamf_installed_group_name, cnx: jamf_cnx
974
+ return unless gr_id
975
+
976
+ @jamf_installed_group_url = "#{jamf_gui_url}/smartComputerGroups.html?id=#{gr_id}&o=r"
977
+ end
978
+
979
+ ####### The Frozen Group
980
+ ###########################################
981
+ ###########################################
982
+
983
+ # @return [Boolean] Does the jamf_frozen_group exist?
984
+ ###########################
985
+ def jamf_frozen_group_exist?
986
+ Jamf::ComputerGroup.all_names(:refresh, cnx: jamf_cnx).include? jamf_frozen_group_name
987
+ end
988
+
989
+ # Create or fetch static in jamf that contains macs with this title 'frozen'
990
+ # If we are deleting and it doesn't exist, return nil.
991
+ # There really isn't any configuration or repairing to do, it's just a static group.
992
+ #
993
+ # @return [Jamf::ComputerGroup, nil] The Jamf ComputerGroup for this title's frozen computers
994
+ #####################################
995
+ def jamf_frozen_group
996
+ return @jamf_frozen_group if @jamf_frozen_group
997
+
998
+ if jamf_frozen_group_exist?
999
+ @jamf_frozen_group = Jamf::ComputerGroup.fetch name: jamf_frozen_group_name, cnx: jamf_cnx
1000
+ else
1001
+ return if deleting?
1002
+
1003
+ progress "Jamf: Creating static group '#{jamf_frozen_group_name}' with no members at the moment", log: :info
1004
+
1005
+ @jamf_frozen_group = Jamf::ComputerGroup.create(
1006
+ name: jamf_frozen_group_name,
1007
+ type: :static,
1008
+ cnx: jamf_cnx
1009
+ )
1010
+ @jamf_frozen_group.save
1011
+
1012
+ end
1013
+ @jamf_frozen_group
1014
+ end
1015
+
1016
+ # Freeze or thaw an array of computers for a title
1017
+ #
1018
+ # @param action [Symbol] :freeze or :thaw
1019
+ #
1020
+ # @param computers [Array<String>, String] The computer name[s] to freeze or thaw. To thaw
1021
+ # all computers pass Xolo::TARGET_ALL (freeze all is not allowed)
1022
+ #
1023
+ # @return [Hash] Keys are computer names, values are Xolo::OK or an error message
1024
+ #################################
1025
+ def freeze_or_thaw_computers(action:, computers:)
1026
+ return unless %i[freeze thaw].include? action
1027
+
1028
+ # convert to an array if it's a single string
1029
+ computers = [computers].flatten
1030
+
1031
+ result, changes_to_log =
1032
+ if action == :thaw
1033
+ thaw_computers(computers: computers)
1034
+ else
1035
+ freeze_computers(computers: computers)
1036
+ end # if action ==
1037
+
1038
+ jamf_frozen_group.save
1039
+
1040
+ unless changes_to_log.empty?
1041
+ action_msg =
1042
+ if action == :freeze
1043
+ "Froze computers: #{changes_to_log.join(', ')}"
1044
+ else
1045
+ "Thawed computers: #{changes_to_log.join(', ')}"
1046
+ end
1047
+
1048
+ log_change msg: action_msg
1049
+ end
1050
+
1051
+ result
1052
+ end
1053
+
1054
+ # freeze some computers
1055
+ # see #freeze_or_thaw_computers
1056
+ ##############
1057
+ def freeze_computers(computers:)
1058
+ result = {}
1059
+ freezes_to_log = []
1060
+
1061
+ comp_names = Jamf::Computer.all_names cnx: jamf_cnx
1062
+ grp_members = jamf_frozen_group.member_names
1063
+
1064
+ computers.each do |comp|
1065
+ if grp_members.include? comp
1066
+ log_info "Not freezing computer '#{comp}' for title '#{title}', already frozen"
1067
+ result[comp] = "#{Xolo::ERROR}: Already frozen"
1068
+ elsif comp_names.include? comp
1069
+ log_info "Freezing computer '#{comp}' for title '#{title}'"
1070
+ jamf_frozen_group.add_member comp
1071
+ result[comp] = Xolo::OK
1072
+ freezes_to_log << comp
1073
+ else
1074
+ log_debug "Cannot freeze computer '#{comp}' for title '#{title}', no such computer"
1075
+ result[comp] = "#{Xolo::ERROR}: No computer with that name"
1076
+ end # if comp_names.include
1077
+ end # computers.each
1078
+
1079
+ [result, freezes_to_log]
1080
+ end
1081
+
1082
+ # thaw some computers
1083
+ # see #freeze_or_thaw_computers
1084
+ ##############
1085
+ def thaw_computers(computers:)
1086
+ result = {}
1087
+ thaws_to_log = []
1088
+
1089
+ if computers.include? Xolo::TARGET_ALL
1090
+ log_info "Thawing all computers for title: '#{title}'"
1091
+ jamf_frozen_group.clear
1092
+ result[Xolo::TARGET_ALL] = Xolo::OK
1093
+ thaws_to_log << Xolo::TARGET_ALL
1094
+ else
1095
+
1096
+ grp_members = jamf_frozen_group.member_names
1097
+ computers.each do |comp|
1098
+ if grp_members.include? comp
1099
+ jamf_frozen_group.remove_member comp
1100
+ log_info "Thawed computer '#{comp}' for title '#{title}'"
1101
+ result[comp] = Xolo::OK
1102
+ thaws_to_log << comp
1103
+ else
1104
+ log_debug "Cannot thaw computer '#{comp}' for title '#{title}', not frozen"
1105
+ result[comp] = "#{Xolo::ERROR}: Not frozen"
1106
+ end # if grp_members.include? comp
1107
+ end # computers.each
1108
+ end # if computers.include?
1109
+
1110
+ [result, thaws_to_log]
1111
+ end
1112
+
1113
+ # Return the members of the 'frozen' static group for a title
1114
+ #
1115
+ # @return [Hash{String => String}] computer name => user name
1116
+ #################################
1117
+ def frozen_computers
1118
+ members = {}
1119
+
1120
+ comps = jamf_frozen_group.member_names
1121
+ comps_to_users = Jamf::Computer.map_all :name, to: :username, cnx: jamf_cnx
1122
+
1123
+ comps.each { |comp| members[comp] = comps_to_users[comp] || 'unknown' }
1124
+
1125
+ members
1126
+ end
1127
+
1128
+ # repair the frozen group
1129
+ ###################
1130
+ def repair_frozen_group
1131
+ progress 'Jamf: Ensuring frozen static group exists', log: :debug
1132
+ # This creates it if it doesn't exist. Nothing more we can do here.
1133
+ jamf_frozen_group
1134
+ end
1135
+
1136
+ # Delete the 'frozen' static group
1137
+ # @return [void]
1138
+ ######################################
1139
+ def delete_jamf_frozen_group
1140
+ grp_id = Jamf::ComputerGroup.valid_id jamf_frozen_group_name, cnx: jamf_cnx
1141
+ return unless grp_id
1142
+
1143
+ progress "Jamf: Deleting static group '#{jamf_frozen_group_name}'", log: :info
1144
+ Jamf::ComputerGroup.delete grp_id, cnx: jamf_cnx
1145
+ end
1146
+
1147
+ # @return [String] the URL for the Frozen static group in Jamf Pro
1148
+ ######################
1149
+ def jamf_frozen_group_url
1150
+ return @jamf_frozen_group_url if @jamf_frozen_group_url
1151
+
1152
+ gr_id = Jamf::ComputerGroup.valid_id jamf_frozen_group_name, cnx: jamf_cnx
1153
+ return unless gr_id
1154
+
1155
+ @jamf_frozen_group_url = "#{jamf_gui_url}/staticComputerGroups.html?id=#{gr_id}&o=r"
1156
+ end
1157
+
1158
+ ####### The Expire Policy
1159
+ ###########################################
1160
+ ###########################################
1161
+
1162
+ # @return [Boolean] Does the jamf_expire_policy exist?
1163
+ #########################
1164
+ def jamf_expire_policy_exist?
1165
+ Jamf::Policy.all_names(:refresh, cnx: jamf_cnx).include? jamf_expire_policy_name
1166
+ end
1167
+
1168
+ # Create or fetch the policy that expires a title
1169
+ #
1170
+ # @return [Jamf::Policy] The Jamf Policy for expiring this title
1171
+ #####################################
1172
+ def jamf_expire_policy
1173
+ return @jamf_expire_policy if @jamf_expire_policy
1174
+
1175
+ if jamf_expire_policy_exist?
1176
+ @jamf_expire_policy = Jamf::Policy.fetch name: jamf_expire_policy_name, cnx: jamf_cnx
1177
+ else
1178
+ return if deleting?
1179
+
1180
+ progress "Jamf: Creating Expiration policy: '#{jamf_expire_policy_name}'", log: :info
1181
+
1182
+ @jamf_expire_policy = Jamf::Policy.create name: jamf_expire_policy_name, cnx: jamf_cnx
1183
+ configure_jamf_expire_policy
1184
+ end
1185
+
1186
+ @jamf_expire_policy
1187
+ end
1188
+
1189
+ # Configure the expiration policy
1190
+ # @param pol [Jamf::Policy] the policy to configure
1191
+ #########################
1192
+ def configure_jamf_expire_policy
1193
+ pol = jamf_expire_policy
1194
+ pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
1195
+ pol.run_command = "#{Xolo::Server::Title::CLIENT_EXPIRE_COMMAND} #{title}"
1196
+ pol.set_trigger_event :checkin, true
1197
+ pol.set_trigger_event :custom, jamf_expire_policy_name
1198
+ pol.scope.add_target(:computer_group, jamf_installed_group_name)
1199
+ pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name] if valid_forced_exclusion_group_name
1200
+ pol.frequency = :daily
1201
+ pol.enable
1202
+ pol.save
1203
+ end
1204
+
1205
+ #############################
1206
+ def delete_jamf_expire_policy
1207
+ return unless jamf_expire_policy_exist?
1208
+
1209
+ progress "Jamf: Deleting expiration policy '#{jamf_expire_policy_name}'", log: :info
1210
+ jamf_expire_policy.delete
1211
+ end
1212
+
1213
+ # repair the expire policy in jamf
1214
+ #####################
1215
+ def repair_jamf_expire_policy
1216
+ if expiration && !expire_paths.pix_empty?
1217
+ progress "Jamf: Repairing expiration policy '#{jamf_expire_policy_name}'"
1218
+ configure_jamf_expire_policy
1219
+
1220
+ else
1221
+ delete_jamf_expire_policy
1222
+ end
1223
+ end
1224
+
1225
+ # @return [String] the URL for the uninstall policy in Jamf Pro
1226
+ ######################
1227
+ def jamf_expire_policy_url
1228
+ return @jamf_expire_policy_url if @jamf_expire_policy_url
1229
+ return unless uninstallable?
1230
+ return unless expiration
1231
+
1232
+ pol_id = Jamf::Policy.valid_id jamf_expire_policy_name, cnx: jamf_cnx
1233
+ return unless pol_id
1234
+
1235
+ @jamf_expire_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
1236
+ end
1237
+
1238
+ ####### The Patch Title
1239
+ ###########################################
1240
+ ###########################################
1241
+
1242
+ # The Jamf Patch Source that is connected to the Title Editor
1243
+ # This must be manually configured in the Jamf server and the Xolo server
1244
+ #
1245
+ # @return [Jamf::PatchSource] The Jamf Patch Source
1246
+ #########################
1247
+ def jamf_ted_patch_source
1248
+ @jamf_ted_patch_source ||=
1249
+ Jamf::PatchSource.fetch(name: Xolo::Server.config.ted_patch_source, cnx: jamf_cnx)
1250
+ end
1251
+
1252
+ # The titles available from the Title Editor via its
1253
+ # Jamf Patch Source. These are titles have have been enabled
1254
+ # in the Title Editor
1255
+ #
1256
+ # available_titles returns a Hash for each available title, with these keys:
1257
+ #
1258
+ # name_id: [String] The Xolo 'title' or the Title Editor 'id'
1259
+ #
1260
+ # current_version: [String] NOTE: This
1261
+ # may be a version that is in 'pilot' from Xolo's POV, but
1262
+ # from the TEd's POV, it has been made available to Jamf.
1263
+ #
1264
+ # publisher: [String]
1265
+ #
1266
+ # last_modified: [Time]
1267
+ #
1268
+ # app_name: [String] The Xolo 'display_name'
1269
+ #
1270
+ # but we map it to just the name_id
1271
+ #
1272
+ # @return [Array<String>] info about the available titles
1273
+ #########################
1274
+ def jamf_ted_available_titles
1275
+ # Don't cache this in an instance var, it changes during the
1276
+ # life of our title instance
1277
+ # jamf_ted_patch_source.available_titles.map { |t| t[:name_id] }
1278
+ # Also NOTE: "available" means not only enabled
1279
+ # in the title editor, but also not already active in jamf.
1280
+ # So any given title will either be here or in
1281
+ # jamf_active_ted_titles, but never both.
1282
+ jamf_ted_patch_source.available_name_ids
1283
+ end
1284
+
1285
+ # @return [Boolean] Is this xolo title available in Jamf?
1286
+ ########################
1287
+ def jamf_ted_title_available?
1288
+ jamf_ted_available_titles.include? title
1289
+ end
1290
+
1291
+ # The titles active in Jamf Patch Management from the Title Editor
1292
+ # This takes into account that other Patch Sources may have titles with the
1293
+ # same 'name_id' (the xolo 'title')
1294
+ # A hash keyed by the title, with values of the jamf title id
1295
+ #
1296
+ # @return [Hash {String => Integer}] The xolo titles that are active in Jamf Patch Management
1297
+ ########################
1298
+ def jamf_active_ted_titles(refresh: false)
1299
+ @jamf_active_ted_titles = nil if refresh
1300
+ return @jamf_active_ted_titles if @jamf_active_ted_titles
1301
+
1302
+ @jamf_active_ted_titles = {}
1303
+ active_from_ted = Jamf::PatchTitle.all(:refresh, cnx: jamf_cnx).select do |t|
1304
+ t[:source_id] == jamf_ted_patch_source.id
1305
+ end
1306
+ active_from_ted.each { |t| @jamf_active_ted_titles[t[:name_id]] = t[:id] }
1307
+ @jamf_active_ted_titles
1308
+ end
1309
+
1310
+ # @return [Boolean] Is this xolo title currently active in Jamf?
1311
+ ########################
1312
+ def jamf_ted_title_active?
1313
+ jamf_active_ted_titles(refresh: true).key? title
1314
+ end
1315
+
1316
+ # The Jamf ID of the Patch Title for this xolo title
1317
+ # if it has been activated in jamf.
1318
+ #
1319
+ # @return [Integer, nil] The Jamf ID of this title, if it is active in Jamf
1320
+ ########################
1321
+ def jamf_patch_title_id
1322
+ @jamf_patch_title_id ||= jamf_active_ted_titles(refresh: true)[title]
1323
+ end
1324
+
1325
+ # create/activate the patch title in Jamf Pro, if not already done.
1326
+ #
1327
+ # This 'subscribes' Jamf to the title in the title editor
1328
+ # It must be enabled in the Title Editor first, meaning
1329
+ # it has at least one requirement, and at least one enabled patch/version.
1330
+ #
1331
+ # The 'stub' patch version should allow this when we create the title.
1332
+ #
1333
+ # Xolo should have enabled it in the Title editor before we
1334
+ # reach this point.
1335
+ #
1336
+ ##########################
1337
+ def activate_jamf_patch_title
1338
+ if jamf_ted_title_active?
1339
+ log_debug "Jamf: Title '#{display_name}' (#{title}) is already active in Jamf"
1340
+ return
1341
+ end
1342
+
1343
+ # wait up to 60secs for the title to become available
1344
+ counter = 0
1345
+ until jamf_ted_title_available? || counter == 12
1346
+ log_debug "Jamf: Waiting for title '#{display_name}' (#{title}) to become available from the Title Editor"
1347
+ sleep 5
1348
+ counter += 1
1349
+ end
1350
+
1351
+ unless jamf_ted_title_available?
1352
+ msg = "Jamf: Title '#{title}' is not yet available to Jamf. Make sure it has at least one version enabled in the Title Editor"
1353
+ log_error msg
1354
+ raise Xolo::NoSuchItemError, msg
1355
+ end
1356
+
1357
+ # This creates/activates the title if needed
1358
+ jamf_patch_title
1359
+
1360
+ accept_jamf_patch_ea
1361
+ end
1362
+
1363
+ # Create or fetch the patch title object for this xolo title.
1364
+ # If we are deleting and it doesn't exist, return nil.
1365
+ #
1366
+ # @param refresh [Boolean] re-fetch the patch title from Jamf?
1367
+ # @return [Jamf::PatchTitle, nil] The Jamf Patch Title for this Xolo Title
1368
+ ########################
1369
+ def jamf_patch_title(refresh: false)
1370
+ @jamf_patch_title = nil if refresh
1371
+ return @jamf_patch_title if @jamf_patch_title
1372
+
1373
+ breaktime = Time.now + Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR
1374
+ until jamf_ted_title_available? || jamf_ted_title_active?
1375
+ if Time.now > breaktime
1376
+ raise Xolo::ServerError,
1377
+ "Jamf: Title '#{title}' is not available in Jamf after waiting #{Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR} seconds"
1378
+ end
1379
+
1380
+ log_debug "Waiting a long time for title '#{title}' to become available from TEd. Checking every 10 secs"
1381
+ sleep 10
1382
+ end
1383
+
1384
+ if jamf_ted_title_active?
1385
+ @jamf_patch_title = Jamf::PatchTitle.fetch(id: jamf_patch_title_id, cnx: jamf_cnx)
1386
+
1387
+ else
1388
+ return if deleting?
1389
+
1390
+ msg = "Jamf: Activating Patch Title '#{display_name}' (#{title}) from the Title Editor Patch Source '#{Xolo::Server.config.ted_patch_source}'"
1391
+ progress msg, log: :info
1392
+
1393
+ @jamf_patch_title =
1394
+ Jamf::PatchTitle.create(
1395
+ name: display_name,
1396
+ source: Xolo::Server.config.ted_patch_source,
1397
+ name_id: title,
1398
+ cnx: jamf_cnx
1399
+ )
1400
+ @jamf_patch_title.category = Xolo::Server::JAMF_XOLO_CATEGORY
1401
+ @jamf_patch_title.save
1402
+
1403
+ end
1404
+ @jamf_patch_title
1405
+ end
1406
+
1407
+ # Delete the patch title
1408
+ # NOTE: jamf api user must have 'delete computer ext. attribs' permmissions
1409
+ ##############################
1410
+ def delete_jamf_patch_title
1411
+ return unless jamf_ted_title_active?
1412
+
1413
+ pt_id = Jamf::PatchTitle.map_all(:id, to: :name_id, cnx: jamf_cnx).invert[title]
1414
+ return unless pt_id
1415
+
1416
+ msg = "Jamf: Deleting (unsubscribing) title '#{display_name}' (#{title}}) in Jamf Patch Management"
1417
+ progress msg, log: :info
1418
+ Jamf::PatchTitle.delete pt_id, cnx: jamf_cnx
1419
+ end
1420
+
1421
+ # @return [String] the URL for the Patch Title in Jamf Pro
1422
+ #####################
1423
+ def jamf_patch_title_url
1424
+ @jamf_patch_title_url ||= "#{jamf_gui_url}/view/computers/patch/#{jamf_patch_title_id}"
1425
+ end
1426
+
1427
+ #### The Manual/SelfService Policy for the currently released version
1428
+ #####################################
1429
+ #####################################
1430
+
1431
+ # @return [Boolean] Does the jamf_manual_install_released_policy exist?
1432
+ #######################
1433
+ def jamf_manual_install_released_policy_exist?
1434
+ Jamf::Policy.all_names(:refresh, cnx: jamf_cnx).include? jamf_manual_install_released_policy_name
1435
+ end
1436
+
1437
+ # Create or fetch the manual install policy for the currently released version.
1438
+ # If we are deleting and it doesn't exist, return nil.
1439
+ # Also return nil if we have no version released
1440
+ #
1441
+ # @return [Jamf::Policy] The manual-install-policy for this version, if it exists
1442
+ ##########################
1443
+ def jamf_manual_install_released_policy
1444
+ @jamf_manual_install_released_policy ||=
1445
+ if jamf_manual_install_released_policy_exist?
1446
+ Jamf::Policy.fetch(name: jamf_manual_install_released_policy_name, cnx: jamf_cnx)
1447
+ else
1448
+ return if deleting?
1449
+
1450
+ create_jamf_manual_install_released_policy
1451
+ end
1452
+ end
1453
+
1454
+ # @return [String] the URL for the manual install policy in Jamf Pro
1455
+ ######################
1456
+ def jamf_manual_install_released_policy_url
1457
+ return @jamf_manual_install_released_policy_url if @jamf_manual_install_released_policy_url
1458
+
1459
+ pol_id = Jamf::Policy.valid_id jamf_manual_install_released_policy_name, cnx: jamf_cnx
1460
+ return unless pol_id
1461
+
1462
+ @jamf_manual_install_released_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
1463
+ end
1464
+
1465
+ # The manual install policy for the current release is always scoped to all
1466
+ # computers, with exclusions
1467
+ #
1468
+ # The policy has a custom trigger, or can be installed via self service if
1469
+ # desired
1470
+ #
1471
+ #########################
1472
+ def create_jamf_manual_install_released_policy
1473
+ msg = "Jamf: Creating manual install policy for current release: '#{jamf_manual_install_released_policy_name}'"
1474
+ progress msg, log: :info
1475
+
1476
+ pol = Jamf::Policy.create name: jamf_manual_install_released_policy_name, cnx: jamf_cnx
1477
+
1478
+ configure_jamf_manual_install_released_policy(pol)
1479
+ pol.save
1480
+ pol
1481
+ end
1482
+
1483
+ # Configure the settings for the manual_install_released_policy
1484
+ # @param pol [Jamf::Policy] the policy we are configuring
1485
+ # @return [void]
1486
+ ###################
1487
+ def configure_jamf_manual_install_released_policy(pol)
1488
+ pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
1489
+ pol.set_trigger_event :checkin, false
1490
+ pol.set_trigger_event :custom, jamf_manual_install_released_policy_name
1491
+ pol.frequency = :ongoing
1492
+ pol.recon = false
1493
+ pol.scope.set_all_targets
1494
+
1495
+ # clear any existing packages
1496
+ pol.package_names.each { |pkg_name| pol.remove_package pkg_name }
1497
+
1498
+ # don't add a package or enable the pol if there's no released version
1499
+ desired_vers = releasing_version || released_version
1500
+ if desired_vers
1501
+ rel_vers = version_object(desired_vers)
1502
+ pol.add_package rel_vers.jamf_pkg_id
1503
+ pol.enable
1504
+ else
1505
+ pol.disable
1506
+ end
1507
+
1508
+ # figure out the exclusions.
1509
+ #
1510
+ # explicit exlusions for the title
1511
+ excls = changes_for_update&.key?(:excluded_groups) ? changes_for_update[:excluded_groups][:new].dup : excluded_groups.dup
1512
+ excls ||= []
1513
+ # plus the frozen group
1514
+ excls << jamf_frozen_group_name
1515
+ # plus any forced group from the server config
1516
+ excls << valid_forced_exclusion_group_name if valid_forced_exclusion_group_name
1517
+ # NOTE: we do not exclude existing installs, so that manual re-installs can be a thing.
1518
+ log_debug "Setting exclusions for manual install policy for current release: #{excls}"
1519
+
1520
+ pol.scope.set_exclusions :computer_groups, excls
1521
+
1522
+ return unless self_service
1523
+
1524
+ if pol.in_self_service?
1525
+ configure_pol_for_self_service(pol)
1526
+ else
1527
+ add_title_to_self_service(pol)
1528
+ end
1529
+ end
1530
+
1531
+ # repair the jamf_manual_install_released_policy - the
1532
+ # policy that installs whatever is the current release
1533
+ #############################
1534
+ def repair_jamf_manual_install_released_policy
1535
+ return unless released_version
1536
+
1537
+ progress 'Jamf: Repairing the manual/Self Service install policy for the current release'
1538
+ pol = jamf_manual_install_released_policy
1539
+ configure_jamf_manual_install_released_policy(pol)
1540
+ pol.save
1541
+ end
1542
+
1543
+ #############################
1544
+ def delete_jamf_manual_install_released_policy
1545
+ return unless jamf_manual_install_released_policy_exist?
1546
+
1547
+ msg = "Jamf: Deleting the manual/Self Service install policy for the current release '#{jamf_manual_install_released_policy_name}'"
1548
+ progress msg, log: :info
1549
+ jamf_manual_install_released_policy.delete
1550
+ end
1551
+
1552
+ # Add the jamf_manual_install_released_policy to self service if needed
1553
+ # @param pol [Jamf::Policy] The jamf_manual_install_released_policy, which may not be saved yet.
1554
+ # @return [void]
1555
+ ##################################
1556
+ def add_title_to_self_service(pol = nil)
1557
+ return unless self_service
1558
+
1559
+ pol ||= jamf_manual_install_released_policy
1560
+
1561
+ msg = "Jamf: Adding Manual Install Policy '#{pol.name}' to Self Service."
1562
+ progress msg, log: :info
1563
+
1564
+ pol.add_to_self_service
1565
+ configure_pol_for_self_service(pol)
1566
+ end
1567
+
1568
+ # Update whether or not we are in self service, based on the setting in the title
1569
+ #
1570
+ #########################
1571
+ def update_ssvc
1572
+ return unless changes_for_update.key? :self_service
1573
+
1574
+ # Update the manual install policy
1575
+ pol = jamf_manual_install_released_policy
1576
+ return unless pol
1577
+
1578
+ # we should be in SSvc - changes_for_update[:self_service][:new] is a boolean
1579
+ if changes_for_update[:self_service][:new]
1580
+ add_title_to_self_service(pol) unless pol.in_self_service?
1581
+
1582
+ # we should not be in SSvc
1583
+ elsif pol.in_self_service?
1584
+ msg = "Jamf: Removing Manual Install Policy '#{pol.name}' from Self Service."
1585
+ progress msg, log: :info
1586
+ pol.remove_from_self_service
1587
+ end
1588
+ pol.save
1589
+
1590
+ # TODO: if we decide to use ssvc in patch policies, loop thru versions to make any changes
1591
+ end
1592
+
1593
+ # Update our self service category, based on the setting in the title
1594
+ # TODO: Allow multiple categories, and 'featuring' ?
1595
+ #
1596
+ #########################
1597
+ def update_ssvc_category
1598
+ return unless changes_for_update.key? :self_service_category
1599
+
1600
+ pol = jamf_manual_install_released_policy
1601
+ return unless pol
1602
+
1603
+ new_cat = changes_for_update[:self_service_category][:new]
1604
+
1605
+ progress(
1606
+ "Jamf: Updating Self Service Category to '#{new_cat}' for Manual Install Policy '#{pol.name}'.",
1607
+ log: :info
1608
+ )
1609
+
1610
+ old_cats = pol.self_service_categories.map { |c| c[:name] }
1611
+ old_cats.each { |c| pol.remove_self_service_category c }
1612
+ pol.add_self_service_category new_cat
1613
+ pol.save
1614
+
1615
+ # TODO: if we decide to use ssvc in patch policies, loop thru versions to make any changes
1616
+ end
1617
+
1618
+ # Update the SSvc Icon for the policies used by this version
1619
+ #
1620
+ # @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
1621
+ # if nil, we'll instantiate it now
1622
+ #
1623
+ # @return [void]
1624
+ ###############################
1625
+ def update_ssvc_icon(ttl_obj: nil)
1626
+ ttl_obj ||= title_object
1627
+ # update manual install policy
1628
+
1629
+ log_debug "Jamf: Updating SSvc Icon for Manual Install Policy '#{jamf_manual_install_policy_name}'"
1630
+ pol = jamf_manual_install_policy
1631
+ return unless pol
1632
+
1633
+ pol.upload :icon, ttl_obj.ssvc_icon_file
1634
+ progress "Jamf: Updated Icon for Manual Install Policy '#{jamf_manual_install_policy_name}'",
1635
+ log: :debug
1636
+
1637
+ # TODO: When we figure out if we want patch policies to use
1638
+ # ssvc - they will need to be updated also
1639
+ end
1640
+
1641
+ # configure the self-service settings of the manual_install_released_policy
1642
+ # @param pol [Jamf::Policy] The jamf_manual_install_released_policy, which may not be saved yet.
1643
+ # @return [void]
1644
+ ############################
1645
+ def configure_pol_for_self_service(pol = nil)
1646
+ pol ||= jamf_manual_install_released_policy
1647
+
1648
+ # clear existing categories, re-add correct one
1649
+ pol.self_service_categories.each { |cat| pol.remove_self_service_category cat }
1650
+ pol.add_self_service_category self_service_category
1651
+
1652
+ pol.self_service_description = description
1653
+ pol.self_service_display_name = display_name
1654
+ pol.self_service_install_button_text = Xolo::Server::Title::SELF_SERVICE_INSTALL_BTN_TEXT
1655
+ return unless ssvc_icon_file
1656
+
1657
+ pol.save # won't do anything unless needed, but has to exist before we can upload icons
1658
+ pol.upload :icon, ssvc_icon_file
1659
+ self.ssvc_icon_id = Jamf::Policy.fetch(id: pol.id, cnx: jamf_cnx).icon.id
1660
+ end
1661
+
1662
+ end # TitleJamfPro
1663
+
1664
+ end # Mixins
1665
+
1666
+ end # Server
1667
+
1668
+ end # Xolo