xolo-server 1.0.1 → 2.0.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/data/client/xolo +152 -79
  3. data/lib/xolo/core/base_classes/title.rb +254 -18
  4. data/lib/xolo/core/base_classes/version.rb +47 -7
  5. data/lib/xolo/core/constants.rb +7 -3
  6. data/lib/xolo/core/security_cmd.rb +128 -0
  7. data/lib/xolo/core/version.rb +1 -1
  8. data/lib/xolo/core.rb +1 -0
  9. data/lib/xolo/server/app.rb +7 -0
  10. data/lib/xolo/server/configuration.rb +243 -37
  11. data/lib/xolo/server/constants.rb +10 -0
  12. data/lib/xolo/server/helpers/auth.rb +19 -2
  13. data/lib/xolo/server/helpers/autopkg.rb +157 -0
  14. data/lib/xolo/server/helpers/client_data.rb +90 -60
  15. data/lib/xolo/server/helpers/file_transfers.rb +412 -82
  16. data/lib/xolo/server/helpers/jamf_pro.rb +30 -7
  17. data/lib/xolo/server/helpers/log.rb +2 -0
  18. data/lib/xolo/server/helpers/maintenance.rb +1 -0
  19. data/lib/xolo/server/helpers/notification.rb +4 -3
  20. data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
  21. data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
  22. data/lib/xolo/server/helpers/subscriptions.rb +119 -0
  23. data/lib/xolo/server/helpers/titles.rb +27 -3
  24. data/lib/xolo/server/helpers/versions.rb +23 -11
  25. data/lib/xolo/server/mixins/changelog.rb +9 -16
  26. data/lib/xolo/server/mixins/title_jamf_access.rb +375 -385
  27. data/lib/xolo/server/mixins/title_ted_access.rb +29 -3
  28. data/lib/xolo/server/mixins/version_jamf_access.rb +95 -112
  29. data/lib/xolo/server/mixins/version_ted_access.rb +25 -0
  30. data/lib/xolo/server/object_locks.rb +2 -1
  31. data/lib/xolo/server/routes/auth.rb +2 -2
  32. data/lib/xolo/server/routes/jamf_pro.rb +11 -1
  33. data/lib/xolo/server/routes/maint.rb +2 -1
  34. data/lib/xolo/server/routes/subscriptions.rb +126 -0
  35. data/lib/xolo/server/routes/title_editor.rb +1 -1
  36. data/lib/xolo/server/routes/titles.rb +26 -11
  37. data/lib/xolo/server/routes/uploads.rb +0 -14
  38. data/lib/xolo/server/routes/versions.rb +14 -13
  39. data/lib/xolo/server/routes.rb +9 -0
  40. data/lib/xolo/server/title.rb +100 -77
  41. data/lib/xolo/server/version.rb +177 -15
  42. data/lib/xolo/server.rb +8 -0
  43. metadata +7 -9
@@ -60,13 +60,16 @@ module Xolo
60
60
  # @return [void]
61
61
  ################################
62
62
  def create_title_in_jamf
63
- # ORDER MATTERS
63
+ log_debug "Display Name in create_title_in_jamf: #{display_name}"
64
64
 
65
- # create the normal ea if needed
66
- configure_jamf_normal_ea if version_script
65
+ # create/activate the Jamf::PatchTitle
66
+ # this notes the jamf id of the PatchTitle
67
+ # and sets the display name if needed for subbed titles
68
+ jamf_patch_title
67
69
 
68
- # must happen after the normal ea is created
69
- configure_jamf_installed_group
70
+ # This creates the installed group
71
+ # must happen after activating the title in jamf
72
+ jamf_installed_group
70
73
 
71
74
  if uninstall_script || !uninstall_ids.pix_empty?
72
75
  configure_jamf_uninstall_script
@@ -82,7 +85,10 @@ module Xolo
82
85
  # Just calling this will create it if it doesn't exist.
83
86
  jamf_frozen_group
84
87
 
85
- activate_jamf_patch_title
88
+ # This either notifies, or does it
89
+ # TODO: EAs for subscribed titles can change at any time and need
90
+ # re-accepted - Notifications must happen.
91
+ accept_jamf_patch_ea
86
92
  end
87
93
 
88
94
  # Apply any changes to Jamf as needed
@@ -94,14 +100,6 @@ module Xolo
94
100
  def update_title_in_jamf
95
101
  # ORDER MATTERS
96
102
 
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
103
  # if the exclusions have changed update the manual install released policy
106
104
  if changes_for_update[:excluded_groups]
107
105
  progress "Jamf: Updating excluded groups for Manual Released Policy '#{jamf_manual_install_released_policy_name}'."
@@ -110,7 +108,6 @@ module Xolo
110
108
 
111
109
  # Do we need to update (vs delete) the uninstall script?
112
110
  if need_to_update_jamf_uninstall_script?
113
-
114
111
  configure_jamf_uninstall_script
115
112
  configure_jamf_uninstall_policy
116
113
 
@@ -122,7 +119,6 @@ module Xolo
122
119
  elsif need_to_delete_jamf_uninstall_script?
123
120
  delete_jamf_uninstall_policy
124
121
  delete_jamf_uninstall_script
125
- # can't expire without
126
122
  end
127
123
 
128
124
  # Do we need to add or delete the expire policy?
@@ -132,16 +128,11 @@ module Xolo
132
128
  changes_for_update.dig(:expiration, :new).to_i.positive? ? jamf_expire_policy : delete_jamf_expire_policy
133
129
  end
134
130
 
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
131
  update_description_in_jamf
140
132
  update_ssvc
141
- update_ssvc_category
142
133
  # TODO: deal with icon changes: if changes_for_update&.key? :self_service_icon
143
134
 
144
- if jamf_ted_title_active?
135
+ if jamf_title_active?
145
136
  update_versions_for_title_changes_in_jamf
146
137
  else
147
138
  log_debug "Jamf: Title '#{display_name}' (#{title}) is not yet active to Jamf, nothing to update in versions."
@@ -151,7 +142,6 @@ module Xolo
151
142
  # Repair this title in Jamf Pro
152
143
  # - TODO: activate title in patch mgmt
153
144
  # - TODO: Accept Patch EA
154
- # - Normal EA 'xolo-<title>-installed-version'
155
145
  # - title-installed smart group 'xolo-<title>-installed'
156
146
  # - frozen static group 'xolo-<title>-frozen'
157
147
  # - manual/SSvc install-current-release policy 'xolo-<title>-install'
@@ -169,7 +159,7 @@ module Xolo
169
159
  ###############################################
170
160
  def repair_jamf_title_objects
171
161
  progress "Jamf: Repairing Jamf objects for title '#{title}'", log: :info
172
- repair_jamf_normal_ea
162
+
173
163
  configure_jamf_installed_group
174
164
  repair_jamf_uninstall_policy
175
165
  repair_jamf_uninstall_script
@@ -187,10 +177,32 @@ module Xolo
187
177
  delete_jamf_uninstall_policy
188
178
  delete_jamf_manual_install_released_policy
189
179
  delete_jamf_uninstall_script
190
- delete_jamf_frozen_group
180
+ delete_lingering_policies_for_title
181
+ sleep 5
182
+
183
+ # must delete this group before the patch title
184
+ # since the group criteria references the patch title
191
185
  delete_jamf_installed_group
192
- delete_jamf_normal_ea
186
+ sleep 5
187
+
193
188
  delete_jamf_patch_title
189
+
190
+ # static group deleted last,
191
+ # was used in scopes for patch and normal policies
192
+ delete_jamf_frozen_group
193
+ end
194
+
195
+ ################################
196
+ def delete_lingering_policies_for_title
197
+ Jamf::Policy.all_names(:refresh, cnx: jamf_cnx).each do |polname|
198
+ next unless polname.start_with? jamf_obj_name_pfx
199
+
200
+ polid = Jamf::Policy.valid_id(polname, cnx: jamf_cnx)
201
+ next unless polid
202
+
203
+ progress "Jamf: Deleting lingering policy #{polname}, possibly from a failed version action.", log: :info
204
+ Jamf::Policy.delete(polid, cnx: jamf_cnx)
205
+ end
194
206
  end
195
207
 
196
208
  # If any title changes require updates to existing versions in
@@ -211,8 +223,6 @@ module Xolo
211
223
  vers_obj.update_release_groups(ttl_obj: self) if changes_for_update&.key? :release_groups
212
224
  vers_obj.update_excluded_groups(ttl_obj: self) if changes_for_update&.key? :excluded_groups
213
225
  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
226
  end
217
227
  end
218
228
 
@@ -300,120 +310,6 @@ module Xolo
300
310
  report
301
311
  end
302
312
 
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
- # nothing to do if its nil, if we need to delete it, that'll happen later
350
- return if version_script_contents.pix_empty?
351
-
352
- progress "Jamf: Configuring regular extension attribute '#{jamf_normal_ea_name}'", log: :info
353
-
354
- jamf_normal_ea.description = "The version of xolo title '#{title}' installed on the machine"
355
- jamf_normal_ea.data_type = 'String'
356
- jamf_normal_ea.input_type = 'script'
357
- jamf_normal_ea.enable
358
- jamf_normal_ea.script = version_script_contents
359
- jamf_normal_ea.save
360
- end
361
-
362
- # Repair the 'normal' EA in jamf to match our version_script
363
- ########################
364
- def repair_jamf_normal_ea
365
- if version_script_contents.pix_empty?
366
- delete_jamf_normal_ea
367
- else
368
- configure_jamf_normal_ea
369
- end
370
- end
371
-
372
- # Delete the 'normal' computer ext attr matching the Patch EA
373
- # @return [void]
374
- ######################################
375
- def delete_jamf_normal_ea
376
- ea_id = Jamf::ComputerExtensionAttribute.valid_id jamf_normal_ea_name, cnx: jamf_cnx
377
- return unless ea_id
378
-
379
- progress "Jamf: Deleting regular extension attribute '#{jamf_normal_ea_name}'", log: :info
380
- Jamf::ComputerExtensionAttribute.delete ea_id, cnx: jamf_cnx
381
- end
382
-
383
- # do we need to update the normal EA in jamf?
384
- # true if our incoming changes include :version_script
385
- # and the new value is not empty (in which case we'll delete it)
386
- #
387
- # @return [Boolean]
388
- ########################
389
- def need_to_update_jamf_normal_ea?
390
- changes_for_update.key?(:version_script) && !changes_for_update[:version_script][:new].pix_empty?
391
- end
392
-
393
- # the script contents of the Normal Jamf EA that comes from our version_script
394
- # @return [String, nil] nil if there is none
395
- ##############################
396
- def jamf_normal_ea_contents
397
- return unless jamf_normal_ea_exist?
398
-
399
- jamf_normal_ea.script
400
- end
401
-
402
- # @return [String] the URL for the Normal EA in Jamf Pro
403
- ######################
404
- def jamf_normal_ea_url
405
- return @jamf_normal_ea_url if @jamf_normal_ea_url
406
- return unless version_script
407
-
408
- ea_id = Jamf::ComputerExtensionAttribute.valid_id jamf_normal_ea_name, cnx: jamf_cnx
409
- return unless ea_id
410
-
411
- # Jamf Changed the URL!
412
- # @jamf_normal_ea_url = "#{jamf_gui_url}/computerExtensionAttributes.html?id=#{ea_id}&o=r"
413
-
414
- @jamf_normal_ea_url = "#{jamf_gui_url}/view/settings/computer-management/computer-extension-attributes/#{ea_id}"
415
- end
416
-
417
313
  ####### The Patch Ext Attribute
418
314
  ###########################################
419
315
  ###########################################
@@ -432,6 +328,8 @@ module Xolo
432
328
  # @return [Boolean]
433
329
  #########################
434
330
  def need_to_accept_jamf_patch_ea?
331
+ return unless managed?
332
+
435
333
  @need_to_accept_jamf_patch_ea
436
334
  end
437
335
 
@@ -457,16 +355,22 @@ module Xolo
457
355
  # @return [void]
458
356
  ############################
459
357
  def accept_jamf_patch_ea
460
- return unless need_to_accept_jamf_patch_ea?
358
+ # give the server a moment to catch up with the new EA before we check on it
359
+ log_debug "Jamf: Checking if we need to accept the version-script EA for title '#{title}'"
360
+ sleep 5
361
+
362
+ awating_acceptance = jamf_patch_ea_awaiting_acceptance?
461
363
 
462
- # return with warning if we aren't auto-accepting
463
- unless Xolo::Server.config.jamf_auto_accept_xolo_eas
464
- 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') "
465
- progress msg
364
+ # return with warning if we aren't auto-accepting, or for all subscribed titles
365
+ if awating_acceptance && (subscribed? || !Xolo::Server.config.jamf_auto_accept_xolo_eas)
366
+ msg = "Jamf: IMPORTANT: The Extension Attribute (version-script) for title '#{title}' must be accepted manually in Jamf Pro at #{jamf_patch_ea_url} under the 'Extension Attribute' tab (click 'Edit'). If you cannot do this yourself, please contact your Xolo server admins for assistance. Deployment will not happen until this is done."
367
+ progress msg, log: :warn, alert: true
466
368
  log_debug 'Admin informed about accepting EA/version-script manually'
467
369
  return
468
370
  end
469
371
 
372
+ return unless need_to_accept_jamf_patch_ea?
373
+
470
374
  # this is true if the Jamf server already knows it needs to be accepted
471
375
  # so just do it now
472
376
  if jamf_patch_ea_awaiting_acceptance?
@@ -607,6 +511,8 @@ module Xolo
607
511
  end
608
512
 
609
513
  # API call to accept the version-script EA in Jamf Pro
514
+ # This never happens for subscribed titles
515
+ #
610
516
  # TODO: when this gets implemented in ruby-jss, use that implementation
611
517
  #
612
518
  # @return [void]
@@ -640,7 +546,7 @@ module Xolo
640
546
  ######################
641
547
  def jamf_patch_ea_url
642
548
  return @jamf_patch_ea_url if @jamf_patch_ea_url
643
- return unless version_script
549
+ return unless version_script || (subscribed? && jamf_patch_ea_data)
644
550
 
645
551
  @jamf_patch_ea_url = "#{jamf_patch_title_url}?tab=extension"
646
552
  end
@@ -786,7 +692,10 @@ module Xolo
786
692
  pol.add_script jamf_uninstall_script_name
787
693
  pol.set_trigger_event :checkin, false
788
694
  pol.set_trigger_event :custom, jamf_uninstall_policy_name
789
- pol.scope.add_target(:computer_group, jamf_installed_group_name)
695
+ # pol.scope.add_target(:computer_group, jamf_installed_group_name)
696
+ # all targets, cus jamf doesn't like some policies with scoped
697
+ # smart groups using 'latest version'
698
+ pol.scope.set_all_targets
790
699
  pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name] if valid_forced_exclusion_group_name
791
700
  pol.frequency = :ongoing
792
701
  pol.enable
@@ -804,7 +713,6 @@ module Xolo
804
713
  end
805
714
 
806
715
  # delete the policy first if it exists
807
-
808
716
  #########################
809
717
  def delete_jamf_uninstall_policy
810
718
  return unless jamf_uninstall_policy_exist?
@@ -861,9 +769,9 @@ module Xolo
861
769
  )
862
770
  @jamf_installed_group.save
863
771
  configure_jamf_installed_group
864
- log_debug 'Jamf: Sleeping to let Jamf server see change to the Installed smart group.'
865
- sleep 10
772
+
866
773
  end
774
+
867
775
  @jamf_installed_group
868
776
  end
869
777
 
@@ -876,75 +784,34 @@ module Xolo
876
784
 
877
785
  jamf_installed_group.criteria = Jamf::Criteriable::Criteria.new(jamf_installed_group_criteria)
878
786
  jamf_installed_group.save
787
+ log_debug 'Jamf: Sleeping 30 secs to let Jamf server see changes to Installed smart group.'
788
+ sleep 30
879
789
  end
880
790
 
881
791
  # The criteria for the smart group in Jamf that contains all Macs
882
792
  # with any version of this title installed
883
793
  #
884
- # If we have, or are about to update to, a version_script (EA) then use it,
885
- # otherwise use the app_name and app_bundle_id.
794
+ # We use the "Patch Reporting: #{display_name}" criterion so that we don't
795
+ # care whether the title uses a version-script or app data.
886
796
  #
887
797
  # @return [Array<Jamf::Criteriable::Criterion>]
888
798
  ###################################
889
799
  def jamf_installed_group_criteria
890
- have_vers_script =
891
- if changes_for_update&.dig :version_script
892
- changes_for_update[:version_script][:new]
893
- else
894
- version_script
895
- end
896
-
897
- # If we have a version_script, use the ea
898
- if have_vers_script
899
- [
900
- Jamf::Criteriable::Criterion.new(
901
- and_or: :and,
902
- name: jamf_normal_ea_name,
903
- search_type: 'is not',
904
- value: Xolo::BLANK
905
- )
906
- ]
907
-
908
- # No version script, so we must be using app data
909
- else
910
- aname = changes_for_update.dig(:app_name, :new) || app_name
911
- abundle = changes_for_update.dig(:app_bundle_id, :new) || app_bundle_id
912
-
913
- [
914
- Jamf::Criteriable::Criterion.new(
915
- and_or: :and,
916
- name: 'Application Title',
917
- search_type: 'is',
918
- value: aname
919
- ),
920
-
921
- Jamf::Criteriable::Criterion.new(
922
- and_or: :and,
923
- name: 'Application Bundle ID',
924
- search_type: 'is',
925
- value: abundle
926
- )
927
- ]
928
- end
929
- end
930
-
931
- # do we need to update the 'installed' smart group?
932
- # true if our incoming changes include the app_name or app_bundle_id
933
- #
934
- # If they changed at all, we need to update no matter what:
935
- # - if they are now nil, we switched to a version script
936
- #
937
- # - if they aren't nil but are different, we need to update
938
- # the group criteria to reflect that.
939
- #
940
- # Changes to the version script, if it was in use before, don't
941
- # require us to change the smart group
942
- #
943
- #
944
- # @return [Boolean]
945
- #########################
946
- def need_to_update_jamf_installed_group?
947
- changes_for_update[:app_name] || changes_for_update[:app_bundle_id]
800
+ [
801
+ Jamf::Criteriable::Criterion.new(
802
+ and_or: :and,
803
+ name: "Patch Reporting: #{display_name}",
804
+ search_type: 'less than or equal',
805
+ value: 'Latest Version'
806
+ ),
807
+
808
+ Jamf::Criteriable::Criterion.new(
809
+ and_or: :or,
810
+ name: "Patch Reporting: #{display_name}",
811
+ search_type: 'is',
812
+ value: 'Unknown Version'
813
+ )
814
+ ]
948
815
  end
949
816
 
950
817
  # Delete the 'installed' smart group
@@ -1239,14 +1106,15 @@ module Xolo
1239
1106
  #
1240
1107
  # @return [Jamf::PatchSource] The Jamf Patch Source
1241
1108
  #########################
1242
- def jamf_ted_patch_source
1243
- @jamf_ted_patch_source ||=
1109
+ def jamf_managed_patch_source
1110
+ @jamf_managed_patch_source ||=
1244
1111
  Jamf::PatchSource.fetch(name: Xolo::Server.config.ted_patch_source, cnx: jamf_cnx)
1245
1112
  end
1246
1113
 
1247
1114
  # The titles available from the Title Editor via its
1248
- # Jamf Patch Source. These are titles have have been enabled
1249
- # in the Title Editor
1115
+ # Jamf Patch Source.
1116
+ # These are titles have have been enabled in the Title Editor
1117
+ # but have not yet been activated in Jamf Patch.
1250
1118
  #
1251
1119
  # available_titles returns a Hash for each available title, with these keys:
1252
1120
  #
@@ -1266,93 +1134,79 @@ module Xolo
1266
1134
  #
1267
1135
  # @return [Array<String>] info about the available titles
1268
1136
  #########################
1269
- def jamf_ted_available_titles
1137
+ def jamf_available_managed_titles
1270
1138
  # Don't cache this in an instance var, it changes during the
1271
1139
  # life of our title instance
1272
- # jamf_ted_patch_source.available_titles.map { |t| t[:name_id] }
1140
+ # jamf_managed_patch_source.available_titles.map { |t| t[:name_id] }
1273
1141
  # Also NOTE: "available" means not only enabled
1274
1142
  # in the title editor, but also not already active in jamf.
1275
1143
  # So any given title will either be here or in
1276
- # jamf_active_ted_titles, but never both.
1277
- jamf_ted_patch_source.available_name_ids
1144
+ # jamf_active_managed_titles, but never both.
1145
+ jamf_managed_patch_source.available_name_ids
1278
1146
  end
1279
1147
 
1280
1148
  # @return [Boolean] Is this xolo title available in Jamf?
1281
1149
  ########################
1282
- def jamf_ted_title_available?
1283
- jamf_ted_available_titles.include? title
1284
- end
1285
-
1286
- # The titles active in Jamf Patch Management from the Title Editor
1287
- # This takes into account that other Patch Sources may have titles with the
1288
- # same 'name_id' (the xolo 'title')
1289
- # A hash keyed by the title, with values of the jamf title id
1290
- #
1291
- # @return [Hash {String => Integer}] The xolo titles that are active in Jamf Patch Management
1292
- ########################
1293
- def jamf_active_ted_titles(refresh: false)
1294
- @jamf_active_ted_titles = nil if refresh
1295
- return @jamf_active_ted_titles if @jamf_active_ted_titles
1296
-
1297
- @jamf_active_ted_titles = {}
1298
- active_from_ted = Jamf::PatchTitle.all(:refresh, cnx: jamf_cnx).select do |t|
1299
- t[:source_id] == jamf_ted_patch_source.id
1300
- end
1301
- active_from_ted.each { |t| @jamf_active_ted_titles[t[:name_id]] = t[:id] }
1302
- @jamf_active_ted_titles
1150
+ def jamf_managed_title_available?
1151
+ jamf_available_managed_titles.include? title
1303
1152
  end
1304
1153
 
1305
1154
  # @return [Boolean] Is this xolo title currently active in Jamf?
1306
1155
  ########################
1307
- def jamf_ted_title_active?
1308
- jamf_active_ted_titles(refresh: true).key? title
1156
+ def jamf_title_active?
1157
+ !jamf_patch_title_id.pix_empty?
1158
+ # if subscribed?
1159
+ # jamf_active_subscribed_titles(refresh: true).key? title
1160
+ # else
1161
+ # jamf_active_managed_titles(refresh: true).key? title
1162
+ # end
1309
1163
  end
1310
1164
 
1311
- # The Jamf ID of the Patch Title for this xolo title
1312
- # if it has been activated in jamf.
1165
+ # The managed titles active in Jamf Patch Management from the Title Editor
1166
+ # A hash keyed by the title, with values of the jamf patch title id
1313
1167
  #
1314
- # @return [Integer, nil] The Jamf ID of this title, if it is active in Jamf
1168
+ # @return [Hash {String => Integer}] The managed xolo titles that are active in Jamf Patch Management
1169
+ # and their Jamf::PatchTitle id
1315
1170
  ########################
1316
- def jamf_patch_title_id
1317
- @jamf_patch_title_id ||= jamf_active_ted_titles(refresh: true)[title]
1318
- end
1171
+ def jamf_active_managed_titles(refresh: false)
1172
+ @jamf_active_managed_titles = nil if refresh
1173
+ return @jamf_active_managed_titles if @jamf_active_managed_titles
1319
1174
 
1320
- # create/activate the patch title in Jamf Pro, if not already done.
1321
- #
1322
- # This 'subscribes' Jamf to the title in the title editor
1323
- # It must be enabled in the Title Editor first, meaning
1324
- # it has at least one requirement, and at least one enabled patch/version.
1325
- #
1326
- # The 'stub' patch version should allow this when we create the title.
1327
- #
1328
- # Xolo should have enabled it in the Title editor before we
1329
- # reach this point.
1330
- #
1331
- ##########################
1332
- def activate_jamf_patch_title
1333
- if jamf_ted_title_active?
1334
- log_debug "Jamf: Title '#{display_name}' (#{title}) is already active in Jamf"
1335
- return
1336
- end
1175
+ @jamf_active_managed_titles = {}
1176
+ all_patch_titles = Jamf::PatchTitle.all(cnx: jamf_cnx)
1337
1177
 
1338
- # wait up to 60secs for the title to become available
1339
- counter = 0
1340
- until jamf_ted_title_available? || counter == 12
1341
- log_debug "Jamf: Waiting for title '#{display_name}' (#{title}) to become available from the Title Editor"
1342
- sleep 5
1343
- counter += 1
1178
+ active_from_ted = all_patch_titles.select do |t|
1179
+ # log_debug "Jamf: Checking if active patch title '#{t[:name]}' with id #{t[:id]} is from the Title Editor's patch source"
1180
+ t[:source_id] == jamf_managed_patch_source.id
1344
1181
  end
1345
1182
 
1346
- unless jamf_ted_title_available?
1347
- msg = "Jamf: Title '#{title}' is not yet available to Jamf. Make sure it has at least one version enabled in the Title Editor"
1348
- log_error msg
1349
- raise Xolo::NoSuchItemError, msg
1183
+ managed_titles = server_app_instance.managed_title_objects.select(&:managed?).map(&:title)
1184
+
1185
+ active_from_ted.each do |t|
1186
+ @jamf_active_managed_titles[t[:name_id]] = t[:id] if managed_titles.include? t[:name_id]
1350
1187
  end
1188
+ @jamf_active_managed_titles
1189
+ end
1351
1190
 
1352
- # This creates/activates the title if needed
1353
- jamf_patch_title
1191
+ # @return [Hash {String => Integer}] The subscribed xolo titles that are active in Jamf Patch Management
1192
+ # and their Jamf::PatchTitle id
1193
+ ########################
1194
+ def jamf_active_subscribed_titles(refresh: false)
1195
+ @jamf_active_subscribed_titles = nil if refresh
1196
+ return @jamf_active_subscribed_titles if @jamf_active_subscribed_titles
1354
1197
 
1355
- accept_jamf_patch_ea
1198
+ @jamf_active_subscribed_titles = {}
1199
+
1200
+ all_active_patchtitles = Jamf::PatchTitle.all :refresh, cnx: jamf_cnx
1201
+
1202
+ server_app_instance.subscribed_title_objects.each do |st|
1203
+ next unless all_active_patchtitles.any? do |pt|
1204
+ pt[:source_id] == st.jamf_patch_source_id && pt[:name_id] == st.title_id
1205
+ end
1206
+
1207
+ @jamf_active_subscribed_titles[st.title] = pt[:id]
1208
+ end
1209
+ @jamf_active_subscribed_titles
1356
1210
  end
1357
1211
 
1358
1212
  # Create or fetch the patch title object for this xolo title.
@@ -1365,52 +1219,118 @@ module Xolo
1365
1219
  @jamf_patch_title = nil if refresh
1366
1220
  return @jamf_patch_title if @jamf_patch_title
1367
1221
 
1368
- breaktime = Time.now + Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR
1369
- until jamf_ted_title_available? || jamf_ted_title_active?
1370
- if Time.now > breaktime
1371
- raise Xolo::ServerError,
1372
- "Jamf: Title '#{title}' is not available in Jamf after waiting #{Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR} seconds"
1373
- end
1374
-
1375
- log_debug "Waiting a long time for title '#{title}' to become available from TEd. Checking every 10 secs"
1376
- sleep 10
1377
- end
1222
+ # wait up to 60secs for a managed title to become available.
1223
+ # subscribed titles are already available
1224
+ wait_for_managed_title_to_become_available
1378
1225
 
1379
- if jamf_ted_title_active?
1226
+ # Jamf::PatchTitle object already active, just fetch it
1227
+ if jamf_patch_title_id # jamf_title_active? || subscribed?
1380
1228
  @jamf_patch_title = Jamf::PatchTitle.fetch(id: jamf_patch_title_id, cnx: jamf_cnx)
1381
1229
 
1230
+ # not yet active, so activate/create the Jamf::PatchTitle object
1382
1231
  else
1383
1232
  return if deleting?
1384
1233
 
1385
- msg = "Jamf: Activating Patch Title '#{display_name}' (#{title}) from the Title Editor Patch Source '#{Xolo::Server.config.ted_patch_source}'"
1386
- progress msg, log: :info
1234
+ @jamf_patch_title = activate_jamf_patch_title
1235
+ end
1236
+ end
1387
1237
 
1388
- @jamf_patch_title =
1389
- Jamf::PatchTitle.create(
1390
- name: display_name,
1391
- source: Xolo::Server.config.ted_patch_source,
1392
- name_id: title,
1393
- cnx: jamf_cnx
1394
- )
1395
- @jamf_patch_title.category = Xolo::Server::JAMF_XOLO_CATEGORY
1396
- @jamf_patch_title.save
1238
+ # activate the patch title in jamf, whatever it's source
1239
+ ############################
1240
+ def activate_jamf_patch_title
1241
+ if jamf_title_active?
1242
+ log_debug "Jamf: Title '#{title}' is already active in Jamf Patch"
1243
+ return
1244
+ end
1397
1245
 
1246
+ # patch_source and title_id are required when adding subbed titles
1247
+ # but pre-defined for managed
1248
+ if managed?
1249
+ log_debug "Jamf: Title '#{title}' is managed, so using pre-defined patch source and title_id"
1250
+ log_debug "Jamf: Display Name in managed?: #{display_name}"
1251
+ self.patch_source = Xolo::Server.config.ted_patch_source
1252
+ self.title_id = title
1253
+ log_debug "Jamf: Display Name in managed? after setting patch_source and title_id: #{display_name}"
1254
+
1255
+ # display name is required for managed titles, but will be empty for subbed
1256
+ # so for subbed, we look it up from the patch source
1257
+ else
1258
+ log_debug "Jamf: Title '#{title}' is subscribed, so looking up patch source and title_id from"
1259
+ log_debug "Jamf: Display Name in NOT managed?: #{display_name}"
1260
+ if display_name.pix_empty?
1261
+ self.display_name = Jamf::PatchSource.available_titles(patch_source, cnx: jamf_cnx).select { |t| t[:name_id] == title_id }.first&.dig :app_name
1262
+ end
1263
+ end # if managed
1264
+
1265
+ log_debug "Jamf: class: #{self.class}, display_name: #{display_name}, patch_source: #{patch_source}, title_id: #{title_id}"
1266
+
1267
+ msg = "Jamf: Activating Patch Title '#{display_name}' (#{title}) from the Patch Source '#{patch_source}'"
1268
+ progress msg, log: :info
1269
+
1270
+ jamf_patch_title =
1271
+ Jamf::PatchTitle.create(
1272
+ name: display_name,
1273
+ source: patch_source,
1274
+ name_id: title_id,
1275
+ cnx: jamf_cnx
1276
+ )
1277
+ jamf_patch_title.category = Xolo::Server::JAMF_XOLO_CATEGORY
1278
+ self.jamf_patch_title_id = jamf_patch_title.save
1279
+ log_debug "Activated Jamf Patch Title '#{display_name}' (#{title}) with id #{jamf_patch_title_id}"
1280
+
1281
+ jamf_patch_title
1282
+ end
1283
+
1284
+ # wait up to 60secs for a managed title to become available to be activated
1285
+ # subscribed titles are already available
1286
+ ####################
1287
+ def wait_for_managed_title_to_become_available
1288
+ return unless managed?
1289
+ return if jamf_title_active?
1290
+
1291
+ counter = 0
1292
+ until jamf_title_active? || jamf_managed_title_available? || counter == 12
1293
+ log_debug "Jamf: Waiting for title '#{display_name}' (#{title}) to become available from the Title Editor"
1294
+ sleep 5
1295
+ counter += 1
1398
1296
  end
1399
- @jamf_patch_title
1297
+
1298
+ return if jamf_managed_title_available? || jamf_title_active?
1299
+
1300
+ msg = "Jamf: Title '#{title}' is not yet available to Jamf. Make sure it has at least one version enabled in the Title Editor"
1301
+ log_error msg
1302
+ raise Xolo::NoSuchItemError, msg
1400
1303
  end
1401
1304
 
1402
1305
  # Delete the patch title
1403
1306
  # NOTE: jamf api user must have 'delete computer ext. attribs' permmissions
1404
1307
  ##############################
1405
1308
  def delete_jamf_patch_title
1406
- return unless jamf_ted_title_active?
1309
+ log_debug "Jamf: Deleting patch title '#{display_name}' (#{title})"
1310
+ unless jamf_title_active?
1311
+ log_debug "Jamf: Patch title '#{display_name}' (#{title}) is not active in Jamf, nothing to delete"
1312
+ return
1313
+ end
1314
+
1315
+ # # subscribed titles often have name_id's like "1B7"
1316
+ # # whereas managed titles have name_id's that are the same as the title,
1317
+ # # so we have to look up the id differently for subbed vs managed
1318
+ # key = subscribed? ? title_id : title
1319
+ # pt_id = Jamf::PatchTitle.map_all(:id, to: :name_id, cnx: jamf_cnx).invert[key]
1407
1320
 
1408
- pt_id = Jamf::PatchTitle.map_all(:id, to: :name_id, cnx: jamf_cnx).invert[title]
1409
- return unless pt_id
1321
+ # unless pt_id
1322
+ # log_debug "Jamf: Cannot find patch title '#{display_name}' (#{title}) in Jamf, cannot delete"
1323
+ # return
1324
+ # end
1410
1325
 
1411
- msg = "Jamf: Deleting (unsubscribing) title '#{display_name}' (#{title}}) in Jamf Patch Management"
1326
+ # # the pt_id SHOULD match the jamf_patch_title_id we have stored, but just in case, we'll use the one we looked up here
1327
+ # unless pt_id.to_s == jamf_patch_title_id.to_s
1328
+ # log_warn "Jamf: Stored patch title id #{jamf_patch_title_id} does not match id #{pt_id} looked up for '#{display_name}' (#{title}). We are deleting the stored id."
1329
+ # end
1330
+
1331
+ msg = "Jamf: Deleting (deactivating) title '#{display_name}' (#{title}}) in Jamf Patch Management"
1412
1332
  progress msg, log: :info
1413
- Jamf::PatchTitle.delete pt_id, cnx: jamf_cnx
1333
+ Jamf::PatchTitle.delete jamf_patch_title_id, cnx: jamf_cnx
1414
1334
  end
1415
1335
 
1416
1336
  # @return [String] the URL for the Patch Title in Jamf Pro
@@ -1476,6 +1396,7 @@ module Xolo
1476
1396
  end
1477
1397
 
1478
1398
  # Configure the settings for the manual_install_released_policy
1399
+ # Be sure to call .save on the policy after this to save the changes.
1479
1400
  # @param pol [Jamf::Policy] the policy we are configuring
1480
1401
  # @return [void]
1481
1402
  ###################
@@ -1490,15 +1411,7 @@ module Xolo
1490
1411
  # clear any existing packages
1491
1412
  pol.package_names.each { |pkg_name| pol.remove_package pkg_name }
1492
1413
 
1493
- # don't add a package or enable the pol if there's no released version
1494
- desired_vers = releasing_version || released_version
1495
- if desired_vers
1496
- rel_vers = version_object(desired_vers)
1497
- pol.add_package rel_vers.jamf_pkg_id
1498
- pol.enable
1499
- else
1500
- pol.disable
1501
- end
1414
+ toggle_jamf_manual_install_released_policy pol
1502
1415
 
1503
1416
  # figure out the exclusions.
1504
1417
  #
@@ -1514,12 +1427,36 @@ module Xolo
1514
1427
 
1515
1428
  pol.scope.set_exclusions :computer_groups, excls
1516
1429
 
1517
- return unless self_service
1430
+ if self_service?
1431
+ add_title_to_self_service(pol)
1432
+ else
1433
+ remove_title_from_self_service(pol)
1434
+ end
1435
+ end
1518
1436
 
1519
- if pol.in_self_service?
1520
- configure_pol_for_self_service(pol)
1437
+ # enable or disable the manual install policy for the current release, depending on
1438
+ # whether or not we have a released version
1439
+ # @param pol [Jamf::Policy] the manual install policy for the current release, which may or may not be saved yet.
1440
+ # return [void]
1441
+ ############################
1442
+ def toggle_jamf_manual_install_released_policy(pol, vobj = nil)
1443
+ # remove any current pkg payload
1444
+ pol.package_ids.each { |pid| pol.remove_package pid }
1445
+
1446
+ unless vobj
1447
+ desired_vers = releasing_version || released_version
1448
+ vobj = desired_vers ? version_object(desired_vers) : nil
1449
+ end
1450
+
1451
+ if vobj
1452
+ progress "Jamf: Setting policy #{jamf_manual_install_released_policy_name} to install the package for version '#{vobj.version}'", log: :info
1453
+ pol.add_package vobj.jamf_pkg_id
1454
+
1455
+ progress "Jamf: Enabling policy #{jamf_manual_install_released_policy_name}", log: :info
1456
+ pol.enable
1521
1457
  else
1522
- add_title_to_self_service(pol)
1458
+ progress "Jamf: Disabling policy #{jamf_manual_install_released_policy_name}, no released version", log: :info
1459
+ pol.disable
1523
1460
  end
1524
1461
  end
1525
1462
 
@@ -1549,111 +1486,164 @@ module Xolo
1549
1486
  # @return [void]
1550
1487
  ##################################
1551
1488
  def add_title_to_self_service(pol = nil)
1552
- return unless self_service
1553
-
1554
1489
  pol ||= jamf_manual_install_released_policy
1555
1490
 
1556
- msg = "Jamf: Adding Manual Install Policy '#{pol.name}' to Self Service."
1557
- progress msg, log: :info
1491
+ unless pol.in_self_service?
1492
+ msg = "Jamf: Adding Manual Install Policy '#{pol.name}' to Self Service."
1493
+ progress msg, log: :info
1494
+ pol.add_to_self_service
1495
+ end
1558
1496
 
1559
- pol.add_to_self_service
1560
1497
  configure_pol_for_self_service(pol)
1498
+ pol.save
1561
1499
  end
1562
1500
 
1563
- # Update whether or not we are in self service, based on the setting in the title
1564
- #
1565
- #########################
1566
- def update_ssvc
1567
- return unless changes_for_update.key? :self_service
1568
-
1569
- # Update the manual install policy
1570
- pol = jamf_manual_install_released_policy
1571
- return unless pol
1501
+ # Add the jamf_manual_install_released_policy to self service if needed
1502
+ # @param pol [Jamf::Policy] The jamf_manual_install_released_policy, which may not be saved yet.
1503
+ # @return [void]
1504
+ ##################################
1505
+ def remove_title_from_self_service(pol = nil)
1506
+ pol ||= jamf_manual_install_released_policy
1507
+ return unless pol.in_self_service?
1572
1508
 
1573
- # we should be in SSvc - changes_for_update[:self_service][:new] is a boolean
1574
- if changes_for_update[:self_service][:new]
1575
- add_title_to_self_service(pol) unless pol.in_self_service?
1509
+ msg = "Jamf: Removing Manual Install Policy '#{pol.name}' from Self Service."
1510
+ progress msg, log: :info
1511
+ pol.remove_from_self_service
1576
1512
 
1577
- # we should not be in SSvc
1578
- elsif pol.in_self_service?
1579
- msg = "Jamf: Removing Manual Install Policy '#{pol.name}' from Self Service."
1580
- progress msg, log: :info
1581
- pol.remove_from_self_service
1582
- end
1583
1513
  pol.save
1584
-
1585
- # TODO: if we decide to use ssvc in patch policies, loop thru versions to make any changes
1586
1514
  end
1587
1515
 
1588
- # Update our self service category, based on the setting in the title
1589
- # TODO: Allow multiple categories, and 'featuring' ?
1516
+ # Update whether or not we are in self service, based on the setting in the title
1590
1517
  #
1591
1518
  #########################
1592
- def update_ssvc_category
1593
- return unless changes_for_update.key? :self_service_category
1594
-
1519
+ def update_ssvc
1595
1520
  pol = jamf_manual_install_released_policy
1596
- return unless pol
1597
1521
 
1598
- new_cat = changes_for_update[:self_service_category][:new]
1522
+ adding_to_ssvc = changes_for_update.dig :self_service, :new
1599
1523
 
1600
- progress(
1601
- "Jamf: Updating Self Service Category to '#{new_cat}' for Manual Install Policy '#{pol.name}'.",
1602
- log: :info
1603
- )
1524
+ updating_config = changes_for_update.dig :self_service_category, :new
1525
+ updating_config ||= changes_for_update.dig :description, :new
1526
+ updating_config ||= changes_for_update.dig :display_name, :new
1604
1527
 
1605
- old_cats = pol.self_service_categories.map { |c| c[:name] }
1606
- old_cats.each { |c| pol.remove_self_service_category c }
1607
- pol.add_self_service_category new_cat
1608
- pol.save
1528
+ # if adding_to_ssvc is nil, we aren't changing it at all
1529
+ # it must be false to indicate removal.
1530
+ removing_from_ssvc = adding_to_ssvc == false
1609
1531
 
1610
- # TODO: if we decide to use ssvc in patch policies, loop thru versions to make any changes
1611
- end
1532
+ if adding_to_ssvc
1533
+ add_title_to_self_service(pol)
1612
1534
 
1613
- # Update the SSvc Icon for the policies used by this version
1614
- #
1615
- # @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
1616
- # if nil, we'll instantiate it now
1617
- #
1618
- # @return [void]
1619
- ###############################
1620
- def update_ssvc_icon(ttl_obj: nil)
1621
- ttl_obj ||= title_object
1622
- # update manual install policy
1535
+ elsif updating_config
1536
+ configure_pol_for_self_service(pol)
1623
1537
 
1624
- log_debug "Jamf: Updating SSvc Icon for Manual Install Policy '#{jamf_manual_install_policy_name}'"
1625
- pol = jamf_manual_install_policy
1626
- return unless pol
1538
+ # we should not be in SSvc, remove it, but leave all the settings
1539
+ elsif removing_from_ssvc
1540
+ remove_title_from_self_service(pol)
1541
+ end
1627
1542
 
1628
- pol.upload :icon, ttl_obj.ssvc_icon_file
1629
- progress "Jamf: Updated Icon for Manual Install Policy '#{jamf_manual_install_policy_name}'",
1630
- log: :debug
1543
+ pol.save
1631
1544
 
1632
- # TODO: When we figure out if we want patch policies to use
1633
- # ssvc - they will need to be updated also
1545
+ # TODO: if we decide to use ssvc in patch policies, loop thru versions to make any changes
1634
1546
  end
1635
1547
 
1636
1548
  # configure the self-service settings of the manual_install_released_policy
1549
+ # NOTE this doesn't actually add it to self service, just configures the settings
1550
+ # See add_title_to_self_service
1551
+ #
1637
1552
  # @param pol [Jamf::Policy] The jamf_manual_install_released_policy, which may not be saved yet.
1638
1553
  # @return [void]
1639
1554
  ############################
1640
1555
  def configure_pol_for_self_service(pol = nil)
1641
1556
  pol ||= jamf_manual_install_released_policy
1642
1557
 
1643
- # clear existing categories, re-add correct one
1644
- pol.self_service_categories.each { |cat| pol.remove_self_service_category cat }
1645
- pol.add_self_service_category self_service_category
1558
+ new_cat = changes_for_update&.dig(:self_service_category, :new) || self_service_category
1559
+ if new_cat
1560
+ progress "Jamf: Setting Self Service category to '#{new_cat}'", log: :debug
1561
+ # clear existing categories, re-add correct one
1562
+ pol.self_service_categories.each { |cat| pol.remove_self_service_category cat }
1563
+ pol.add_self_service_category new_cat if new_cat
1564
+ end
1565
+
1566
+ new_display_name = changes_for_update&.dig(:self_service_display_name, :new) || display_name
1567
+ progress "Jamf: Setting Self Service display name to '#{new_display_name}'", log: :debug
1568
+ pol.self_service_display_name = new_display_name
1569
+
1570
+ new_desc = changes_for_update&.dig(:self_service_description, :new) || description
1571
+ progress "Jamf: Setting Self Service description to '#{new_desc}'", log: :debug
1572
+ pol.self_service_description = new_desc
1646
1573
 
1647
- pol.self_service_description = description
1648
- pol.self_service_display_name = display_name
1649
1574
  pol.self_service_install_button_text = Xolo::Server::Title::SELF_SERVICE_INSTALL_BTN_TEXT
1650
1575
  return unless ssvc_icon_file
1651
1576
 
1577
+ progress 'Jamf: Uploading Self Service icon', log: :debug
1652
1578
  pol.save # won't do anything unless needed, but has to exist before we can upload icons
1653
1579
  pol.upload :icon, ssvc_icon_file
1580
+ # re-fetch the pol to get the icon id
1654
1581
  self.ssvc_icon_id = Jamf::Policy.fetch(id: pol.id, cnx: jamf_cnx).icon.id
1655
1582
  end
1656
1583
 
1584
+ # run the autopkg recipe if defined
1585
+ # See Helpers::AutoPkg for more details
1586
+ ############################
1587
+ def run_autopkg_recipe
1588
+ server_app_instance.run_autopkg_recipe self
1589
+ end
1590
+
1591
+ # Methods used by subscription stuff
1592
+ ##############################
1593
+
1594
+ # Returns an array of known patch versions for this title from Jamf
1595
+ # These are not necessarily in xolo, but they are available from the Patch Source.
1596
+ #
1597
+ # One or more Hashes like this:
1598
+ # {
1599
+ # "version": "143.0.7499.170",
1600
+ # "releaseDate": "2025-12-18T20:31:01Z",
1601
+ # "standalone": true,
1602
+ # "minimumOperatingSystem": "12.0",
1603
+ # "rebootRequired": false,
1604
+ # "killApps": [
1605
+ # {
1606
+ # "appName": "Google Chrome"
1607
+ # }
1608
+ # ],
1609
+ # "absoluteOrderId": "0"
1610
+ # }
1611
+ #
1612
+ # @param version [String, Symbol, nil] If a string, only return info about that version, if :latest,
1613
+ # only the latest version, otherwise all available versions known by the patch source.
1614
+ #
1615
+ # @param refresh [Boolean] re-read the data from the Jamf Pro API
1616
+ #
1617
+ # @return [Array<Hash>] Data about available patch versions for this title
1618
+ ################################
1619
+ def patch_versions(version: nil, refresh: false)
1620
+ @patch_versions = nil if refresh
1621
+ @patch_versions ||= []
1622
+
1623
+ if @patch_versions.empty?
1624
+ page = 0
1625
+ loop do
1626
+ jpapi_path = "#{Xolo::Server::JPAPI_PATCH_TITLE_ENDPOINT}/#{jamf_patch_title_id}/definitions?page=#{page}&page-size=1000&sort=absoluteOrderId%3Aasc"
1627
+ log_debug "Jamf: Fetching patch versions for title '#{title}' from Jamf API endpoint '#{jpapi_path}'"
1628
+
1629
+ data = jamf_cnx.jp_get jpapi_path
1630
+ break if data[:results].empty?
1631
+
1632
+ @patch_versions += data[:results]
1633
+ page += 1
1634
+ end # loop
1635
+ end # if
1636
+
1637
+ case version
1638
+ when nil
1639
+ @patch_versions
1640
+ when :latest
1641
+ @patch_versions.select { |p| p[:absoluteOrderId] == '0' }
1642
+ else
1643
+ @patch_versions.select { |p| p[:version] == version }
1644
+ end # case
1645
+ end # patch_versions
1646
+
1657
1647
  end # TitleJamfPro
1658
1648
 
1659
1649
  end # Mixins