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,1541 @@
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::Version
18
+ # to define Version-related access to the Jamf Pro server
19
+ #
20
+ module VersionJamfAccess
21
+
22
+ # Constants
23
+ #
24
+ ##############################
25
+ ##############################
26
+
27
+ # The group of macs with this version installed
28
+ # is named the full prefix plus this suffix.
29
+ JAMF_SMART_GROUP_NAME_INSTALLED_SFX = '-installed'
30
+
31
+ # The policy that does initial installs on-demand
32
+ # (via 'xolo install <title> <version') is named the full
33
+ # prefix plus this suffix.
34
+ JAMF_POLICY_NAME_MANUAL_INSTALL_SFX = '-manual-install'
35
+
36
+ # The policy that does auto-installs is named the full
37
+ # prefix plus this suffix.
38
+ # The scope is changed as needed when a version's status
39
+ # changes
40
+ JAMF_POLICY_NAME_AUTO_INSTALL_SFX = '-auto-install'
41
+
42
+ # The policy that does auto-re-installs is named the full
43
+ # prefix plus this suffix.
44
+ # The scope is changed as needed when a version's status
45
+ # changes
46
+ JAMF_POLICY_NAME_AUTO_REINSTALL_SFX = '-auto-reinstall'
47
+
48
+ # How long to wait after a pkg re-upload before creating/enabling/flushing
49
+ # the auto-reinstall policy
50
+ # See TODO in #wait_to_enable_reinstall_policy
51
+ JAMF_AUTO_REINSTALL_WAIT_SECS = 15 * 60
52
+
53
+ # POLICIES, PATCH POLICIES, SCOPING
54
+ #############################
55
+ #
56
+ # SMART GROUPS
57
+ # For each title there will be a smart group containing all macs that have any version
58
+ # of the title installed. The smart group will be named 'xolo-<title>-installed'
59
+ #
60
+ # It will be used as an exclusion for the auto-initial-installation policy for each version
61
+ # since if the title is installed at all, any installation is not 'initial' but an update, and
62
+ # will be handled by the Patch Policy.
63
+ #
64
+ # Since there is one per title, it's name is stored in the title object's #jamf_installed_group_name
65
+ # attribute, and the title object has has the method for creating it.
66
+ # It will be created when the first version is added to the title.
67
+ #
68
+ # POLICIES
69
+ # Each version gets two policies for initial installation
70
+ #
71
+ # - one for auto-installs called 'xolo-<title>-<version>-auto-install'
72
+ # - xolo server maintains the scope as needed
73
+ # - Targeted to pilot-groups first, then release-groups when released
74
+ # - Excluded for excluded groups and frozen-groups
75
+ # - never in self service
76
+ #
77
+ # - one for manual installs called 'xolo-<title>-<version>-manual-install'
78
+ # and self-service installs
79
+ # - xolo server maintains the scope as needed
80
+ # - Targeted to all with this trigger xolo-install-<target>-<version>
81
+ # - Excluded for excluded groups and frozen-groups
82
+ # - the xolo client will determine which is released when
83
+ # running 'xolo install <title>'
84
+ #
85
+ # NOTE: Other install policies can be created manually for other purposes, just
86
+ # don't name them with xolo-ish names
87
+ #
88
+ # PATCH POLICIES
89
+ # Each version gets one patch policy
90
+ #
91
+ # The patch policy is first scoped targeted to pilot groups.
92
+ # Excluded for excluded groups and frozen-groups
93
+ #
94
+ # When the version is released, the scope is changed to All
95
+ #
96
+ # NOTE: remember that patch polices are pre-limited to only 'eligible'
97
+ # machines - those that have a lower version installed and meet other
98
+ # conditions.
99
+ #
100
+ # But.... questions...
101
+ #
102
+ # Should it act like d3, and auto-install updates always?
103
+ # def. for auto-install groups... but how about for the general
104
+ # populace, like those who installed initially via SSvc?? Should it
105
+ # auto-update, or notify them to do it in SSvc?
106
+ #
107
+ # If d3-like behaviour:
108
+ # - it auto-installs for anyone who has any version installed
109
+ # - at first, scoped to any pilot-groups, they'll get the latest version
110
+ # - when released, re-scoped to 'all' (see note below)
111
+ #
112
+ #
113
+ # NOTE: Other patch policies can be created manually for other purposes, just
114
+ # don't name them with xolo-ish names
115
+ #
116
+ #####################
117
+ #
118
+ # install live
119
+ # => xolo install title
120
+ #
121
+ # runs 'jamf policy -trigger xolo-install-current-<title>'
122
+ # the xolo server maintains the trigger
123
+ #################
124
+ #
125
+ # install pilot
126
+ # => xolo install title version
127
+ #
128
+ # runs 'jamf policy -trigger xolo-install-<title>-<version>'
129
+ # the xolo server maintains the trigger
130
+ ##################
131
+ #
132
+ # auto-install on pilot groups or target groups
133
+ # => xolo sync
134
+ #
135
+ # runs 'jamf policy'
136
+ # the xolo server maintains the scopes for the policies
137
+ # patch policies will be run as needed
138
+ ##################
139
+ #
140
+ # get the lates JSON data about titles and versions
141
+ # => xolo update
142
+ #
143
+ # runs 'jamf policy -trigger xolo-update'
144
+ # the xolo server maintains a package that deploys the JSON file
145
+ ##################
146
+ #
147
+ # list available titles or versions
148
+ # => xolo list-titles
149
+ #
150
+ # reads from a local JSON file of title & version data
151
+ # maintained by the xolo server and pushed out via
152
+ # a checkin policy
153
+ ##################
154
+
155
+ # Module methods
156
+ #
157
+ # These are available as module methods but not as 'helper'
158
+ # methods in sinatra routes & views.
159
+ #
160
+ ##############################
161
+ ##############################
162
+
163
+ # when this module is included
164
+ ##############################
165
+ def self.included(includer)
166
+ Xolo.verbose_include includer, self
167
+ end
168
+
169
+ # when this module is extended
170
+ def self.extended(extender)
171
+ Xolo.verbose_extend extender, self
172
+ end
173
+
174
+ # Instance methods
175
+ #
176
+ # These are available directly in sinatra routes and views
177
+ #
178
+ ##############################
179
+
180
+ ####### The Xolo Version itself
181
+ ###########################################
182
+ ###########################################
183
+
184
+ # Create everything we need in Jamf
185
+ ############################
186
+ def create_in_jamf
187
+ # this will create the JPackage object
188
+ jamf_package
189
+
190
+ # The
191
+ # - jamf_installed_group
192
+ # - jamf_auto_reinstall_policy
193
+ # aren't needed until there's a pkg re-upload
194
+ # and they will be created then if they doesn't already exist
195
+
196
+ # these will create the policies
197
+ jamf_auto_install_policy
198
+ jamf_manual_install_policy
199
+
200
+ activate_patch_version_in_jamf
201
+ end
202
+
203
+ # Apply edits to the Xolo version to Jamf as needed
204
+ # This includes scope changes in policies, changes to pkg 'reboot' setting
205
+ # and changes to pkg 'os_requirements'
206
+ # Uploading a new .pkg installer happen separately
207
+ #########################################
208
+ def update_version_in_jamf
209
+ update_pilot_groups if changes_for_update&.key? :pilot_groups
210
+ update_release_groups(ttl_obj: title_object) if changes_for_update&.key? :release_groups
211
+ update_excluded_groups(ttl_obj: title_object) if changes_for_update&.key? :excluded_groups
212
+
213
+ update_jamf_pkg_reboot if changes_for_update&.key? :reboot
214
+ update_jamf_pkg_min_os if changes_for_update&.key? :min_os
215
+
216
+ # TODO: Update the critera for the jamf_installed_group IF
217
+ #
218
+ # - the group exists AND
219
+ # - the title has changed how it determines installed versions, e.g. by adding or
220
+ # changing a version_script or app_bundle_id
221
+ # Changing those is very rare, and ill-advised, so we will implement
222
+ # this later.
223
+ #
224
+ # if jamf_installed_group_exist?
225
+ # end
226
+ end
227
+
228
+ # Validate and fix any Jamf::JPackage objects that
229
+ # related to this version:
230
+ # - the package object
231
+ # - the installed-group
232
+ # - the auto-install policy
233
+ # - the manual-install policy
234
+ # - the auto-reinstall policy
235
+ # - the patch policy
236
+ #########################################
237
+ def repair_jamf_version_objects
238
+ repair_jamf_package
239
+ repair_jamf_installed_group # wont happen if no reupload_date
240
+ repair_jamf_auto_install_policy
241
+ repair_jamf_manual_install_policy
242
+ repair_jamf_auto_reinstall_policy # wont happen if no reupload_date
243
+ repair_jamf_patch_policy
244
+ end
245
+
246
+ # Delete an entire version from Jamf Pro
247
+ # This includes the package, the manual install policy, the auto install policy,
248
+ # and the patch policy.
249
+ #
250
+ # @return [void]
251
+ #
252
+ #########################
253
+ def delete_version_from_jamf
254
+ log_debug "Deleting Version '#{version}' from Jamf"
255
+
256
+ # The Policies
257
+ pols = [
258
+ jamf_manual_install_policy,
259
+ jamf_auto_install_policy,
260
+ jamf_auto_reinstall_policy,
261
+ jamf_patch_policy
262
+ ]
263
+ pols.each do |pol|
264
+ next unless pol
265
+
266
+ progress "Jamf: Deleting #{pol.class} '#{pol.name}'", log: :info
267
+ pol.delete
268
+ end
269
+
270
+ # The Installed Group
271
+ if jamf_installed_group
272
+ progress "Jamf: Deleting #{jamf_installed_group.class} '#{jamf_installed_group.name}'", log: :info
273
+ jamf_installed_group.delete
274
+ end
275
+
276
+ # Delete package object
277
+ # This is slow and it blocks, so do it in a thread and update progress every
278
+ # 15 secs
279
+ return unless Jamf::JPackage.valid_id packageName: jamf_pkg_name, cnx: jamf_cnx
280
+
281
+ delete_jamf_package
282
+
283
+ # The code below is used when we want real-time progress updates to xadm
284
+ # while deletion is happening. It's slow, so we're not using it now.
285
+
286
+ # msg = "Jamf: Starting deletion of Package '#{jamf_pkg_name}' id #{jamf_pkg_id} at #{Time.now.strftime '%F %T'}..."
287
+ # progress msg, log: :debug
288
+
289
+ # # do this in another thread, so we can report the progress while its happening
290
+ # pkg_del_thr = Thread.new { Jamf::JPackage.fetch(packageName: jamf_pkg_name, cnx: jamf_cnx).delete }
291
+ # pkg_del_thr.name = "package-deletion-thread-#{session[:xolo_id]}"
292
+ # sleep 15
293
+ # while pkg_del_thr.alive?
294
+ # progress "... #{Time.now.strftime '%F %T'} still deleting, this is slow, sorry."
295
+ # sleep 15
296
+ # end
297
+
298
+ # msg = "Jamf: Deleted Package '#{jamf_pkg_name}' id #{jamf_pkg_id} at #{Time.now.strftime '%F %T'}"
299
+ # progress msg, log: :debug
300
+ end
301
+
302
+ # @return [String] The start of the Jamf Pro URL for GUI/WebApp access
303
+ ################
304
+ def jamf_gui_url
305
+ server_app_instance.jamf_gui_url
306
+ end
307
+
308
+ ####### The Jamf Package Object
309
+ ###########################################
310
+ ###########################################
311
+
312
+ # Create or fetch the Jamf::JPackage object for this version
313
+ # Returns nil if the package doesn't exist and we're deleting
314
+ #
315
+ # @return [Jamf::JPackage] the Package object associated with this version
316
+ ######################
317
+ def jamf_package
318
+ return @jamf_package if @jamf_package
319
+
320
+ id = jamf_pkg_id || Jamf::JPackage.valid_id(name: jamf_pkg_name, cnx: jamf_cnx)
321
+ @jamf_package =
322
+ if id
323
+ log_debug "Jamf: Fetching Jamf::JPackage '#{id}'"
324
+ Jamf::JPackage.fetch id: id, cnx: jamf_cnx
325
+ else
326
+ return if deleting?
327
+
328
+ create_jamf_package
329
+ end
330
+ end
331
+
332
+ # @return [Jamf::JPackage] Create the Jamf::JPackage object for this version and return it
333
+ #########################
334
+ def create_jamf_package
335
+ progress "Jamf: Creating Package object '#{jamf_pkg_name}'", log: :info
336
+
337
+ # The filename is temporary, and will be replaced when the file is uploaded
338
+ pkg = Jamf::JPackage.create(
339
+ cnx: jamf_cnx,
340
+ packageName: jamf_pkg_name,
341
+ fileName: "#{jamf_pkg_name}.pkg",
342
+ rebootRequired: reboot,
343
+ notes: jamf_package_notes,
344
+ categoryId: jamf_xolo_category_id,
345
+ osRequirements: ">=#{min_os}"
346
+ )
347
+
348
+ # TODO: Implement max_os, either here, or by maintaining a smart group?
349
+ # I really wish jamf would improve how package objects handle
350
+ # OS requirements, building in the concept of min/max
351
+
352
+ self.jamf_pkg_id = pkg.save
353
+ # save the data now so the pkg_id is available for immeadiate use, e.g. by pkg upload
354
+ save_local_data
355
+ pkg
356
+ rescue => e
357
+ msg = "Jamf: Failed to create Jamf::JPackage '#{jamf_pkg_name}': #{e.class}: #{e}"
358
+ log_error msg
359
+ raise Xolo::ServerError, msg
360
+ end
361
+
362
+ # @return [String] the 'notes' text for the Jamf::JPackage object for this version
363
+ #############################
364
+ def jamf_package_notes(ttl_obj: nil)
365
+ ttl_obj ||= title_object
366
+ pkg_notes = Xolo::Server::Version::JAMF_PKG_NOTES_PREFIX.sub(
367
+ Xolo::Server::Version::JAMF_PKG_NOTES_VERS_PH,
368
+ version
369
+ )
370
+ pkg_notes.sub!(
371
+ Xolo::Server::Version::JAMF_PKG_NOTES_TITLE_PH,
372
+ title
373
+ )
374
+ desc = ttl_obj.changes_for_update.dig(:description, :new) || ttl_obj.description
375
+ pkg_notes << desc
376
+ pkg_notes
377
+ end
378
+
379
+ # update the description for the Jamf::JPackage
380
+ # @return [void]
381
+ def update_jamf_package_notes(ttl_obj: nil)
382
+ ttl_obj ||= title_object
383
+ progress "Jamf: Updating notes for Jamf::JPackage '#{jamf_pkg_name}'", log: :debug
384
+ jamf_package.notes = jamf_package_notes(ttl_obj: ttl_obj)
385
+ jamf_package.save
386
+ end
387
+
388
+ # update the reboot setting for the Jamf::JPackage
389
+ # @return [void]
390
+ ##########################
391
+ def update_jamf_pkg_reboot
392
+ new_reboot = changes_for_update&.key?(:reboot) ? changes_for_update[:reboot][:new] : reboot
393
+ progress "Jamf: Updating reboot setting for Jamf::JPackage '#{jamf_pkg_name}' to '#{new_reboot}'", log: :debug
394
+ jamf_package.rebootRequired = new_reboot
395
+ jamf_package.save
396
+ end
397
+
398
+ # update the min_os setting for the Jamf::JPackage
399
+ # @return [void]
400
+ ##########################
401
+ def update_jamf_pkg_min_os
402
+ new_min = changes_for_update&.key?(:min_os) ? changes_for_update[:min_os][:new] : min_os
403
+ progress "Jamf: Updating os_requirement for Jamf::JPackage '#{jamf_pkg_name}' to '#{new_min}'",
404
+ log: :debug
405
+ jamf_package.osRequirements = ">=#{new_min}"
406
+ jamf_package.save
407
+ end
408
+
409
+ # repair the package object only
410
+ #############################
411
+ def repair_jamf_package
412
+ # If these values are all correct, nothing will be saved
413
+ progress "Jamf: Repairing Package '#{jamf_pkg_name}'", log: :info
414
+ jamf_package.packageName = jamf_pkg_name
415
+ jamf_package.fileName = "#{jamf_pkg_name}.pkg"
416
+ jamf_package.rebootRequired = reboot
417
+ jamf_package.notes = jamf_package_notes
418
+ jamf_package.categoryId = jamf_xolo_category_id
419
+ jamf_package.osRequirements = ">=#{min_os}"
420
+ jamf_package.save
421
+ end
422
+
423
+ # @return [String] the URL for the Package that installs this version in Jamf Pro
424
+ ######################
425
+ def jamf_package_url
426
+ return @jamf_package_url if @jamf_package_url
427
+ return unless jamf_pkg_id
428
+
429
+ # the old url
430
+ # @jamf_package_url = "#{jamf_gui_url}/packages.html?id=#{jamf_pkg_id}&o=r"
431
+
432
+ @jamf_package_url = "#{jamf_gui_url}/view/settings/computer-management/packages/#{jamf_pkg_id}?tab=general"
433
+ # https://casper.pixar.com:8443/view/settings/computer-management/packages/12042?tab=general
434
+ end
435
+
436
+ # Delete the package for this version from Jamf Pro.
437
+ # Package deletion takes a long time, so we do it in a threadpool
438
+ # and tell the admin to check the Alert Tool for completion
439
+ # (if we have an alert tool in place) or to wait at least 5 min before
440
+ # re-adding the same version.
441
+ #
442
+ # @return [void]
443
+ #########################
444
+ def delete_jamf_package
445
+ pkg_id = Jamf::JPackage.valid_id packageName: jamf_pkg_name, cnx: jamf_cnx
446
+ return unless pkg_id
447
+
448
+ msg = "Jamf: Starting deletion of Package '#{jamf_pkg_name}' id #{jamf_pkg_id} at #{Time.now.strftime '%F %T'}"
449
+ progress msg, log: :info
450
+
451
+ warning = +"IMPORTANT: Package deletion is slow. If you plan to re-add this version, '#{version}', please\n "
452
+ warning <<
453
+ if Xolo::Server.config.alert_tool
454
+ 'check your Xolo alerts for completion, which can take up to 5 minutes,'
455
+ else
456
+ 'wait at least 5 minutes'
457
+ end
458
+ warning << ' before re-adding this version.'
459
+
460
+ progress warning, log: nil
461
+
462
+ self.class.pkg_deletion_pool.post do
463
+ start = Time.now
464
+ log_info "Jamf: Started threadpool deletion of Package '#{jamf_pkg_name}' id #{jamf_pkg_id} at #{start}"
465
+ jamf_cnx.timeout = 3600
466
+ Jamf::JPackage.delete pkg_id, cnx: jamf_cnx
467
+ finish = Time.now
468
+ duration = (finish - start).to_i.pix_humanize_secs
469
+ log_info "Jamf: Deleted Package '#{jamf_pkg_name}' id #{jamf_pkg_id} in #{duration}", alert: true
470
+ rescue => e
471
+ log_error "Package Deletion thread: #{e.class}: #{e}"
472
+ e.backtrace.each { |l| log_error "..#{l}" }
473
+ end
474
+ end
475
+
476
+ ####### The Jamf Auto Install Policy
477
+ ###########################################
478
+ ###########################################
479
+
480
+ # @return [Boolean] does the jamf_auto_install_policy exist?
481
+ ###########################
482
+ def jamf_auto_install_policy_exist?
483
+ Jamf::Policy.all_names(cnx: jamf_cnx).include? jamf_auto_install_policy_name
484
+ end
485
+
486
+ # Create or fetch the auto install policy for this version
487
+ # If we are deleting and it doesn't exist, return nil.
488
+ #
489
+ # @return [Jamf::Policy] The auto-install-policy for this version, if it exists
490
+ ##########################
491
+ def jamf_auto_install_policy
492
+ @jamf_auto_install_policy ||=
493
+ if jamf_auto_install_policy_exist?
494
+ Jamf::Policy.fetch(name: jamf_auto_install_policy_name, cnx: jamf_cnx)
495
+ else
496
+ return if deleting?
497
+
498
+ create_jamf_auto_install_policy
499
+ end
500
+ end
501
+
502
+ # The auto install policy is triggered by checkin
503
+ # but may have narrow scope targets, or may be
504
+ # targeted to 'all' (after release)
505
+ # Before release, the targets are those defined in #pilot_groups_to_use
506
+ #
507
+ # After release, the targets are changed to those
508
+ # in title_object#target_group
509
+ #
510
+ # This policy is never in self service
511
+ # @return [Jamf::Policy] the auto install policy for this version
512
+ #########################
513
+ def create_jamf_auto_install_policy
514
+ progress "Jamf: Creating Auto Install Policy: #{jamf_auto_install_policy_name}", log: :debug
515
+ pol = Jamf::Policy.create name: jamf_auto_install_policy_name, cnx: jamf_cnx
516
+ configure_jamf_auto_install_policy(pol)
517
+ pol.save
518
+ pol
519
+ end
520
+
521
+ # repair the auto-install policy only
522
+ #############################
523
+ def repair_jamf_auto_install_policy
524
+ progress "Jamf: Repairing Auto Install Policy '#{jamf_auto_install_policy_name}'", log: :info
525
+ pol = jamf_auto_install_policy
526
+ configure_jamf_auto_install_policy(pol)
527
+ pol.save
528
+ end
529
+
530
+ # Configure the given policy as the auto-install policy for this version
531
+ # @param pol [Jamf::Policy] the policy to configure
532
+ ################################
533
+ def configure_jamf_auto_install_policy(pol)
534
+ pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
535
+ pol.set_trigger_event :checkin, true
536
+ pol.set_trigger_event :custom, Xolo::BLANK
537
+ pol.frequency = :once_per_computer
538
+ pol.retry_event = :checkin
539
+ pol.retry_attempts = 5
540
+ pol.recon = false
541
+
542
+ pol.package_names.each { |pkg_name| pol.remove_package pkg_name }
543
+ pol.add_package jamf_pkg_name
544
+
545
+ # exclusions are for always
546
+ set_policy_exclusions pol
547
+
548
+ # set the scope targets based on status
549
+ if pilot?
550
+ set_policy_pilot_groups pol
551
+ else
552
+ set_policy_release_groups pol
553
+ end
554
+
555
+ # enable or disable based on status
556
+ if pilot? || released?
557
+ pol.enable
558
+ else
559
+ pol.disable
560
+ end
561
+ end
562
+
563
+ # @return [String] the URL for the Jamf Pro Policy that does auto-installs of this version
564
+ ######################
565
+ def jamf_auto_install_policy_url
566
+ return @jamf_auto_install_policy_url if @jamf_auto_install_policy_url
567
+
568
+ pol_id = Jamf::Policy.valid_id jamf_auto_install_policy_name, cnx: jamf_cnx
569
+ return unless pol_id
570
+
571
+ @jamf_auto_install_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
572
+ end
573
+
574
+ ####### The Jamf Manual Install Policy
575
+ ###########################################
576
+ ###########################################
577
+
578
+ # @return [Boolean] does the jamf_manual_install_policy exist?
579
+ ###########################
580
+ def jamf_manual_install_policy_exist?
581
+ Jamf::Policy.all_names(cnx: jamf_cnx).include? jamf_manual_install_policy_name
582
+ end
583
+
584
+ # Create or fetch the manual install policy for this version
585
+ # If we are deleting and it doesn't exist, return nil.
586
+ #
587
+ # @return [Jamf::Policy] The manual-install-policy for this version, if it exists
588
+ ##########################
589
+ def jamf_manual_install_policy
590
+ @jamf_manual_install_policy ||=
591
+ if jamf_manual_install_policy_exist?
592
+ Jamf::Policy.fetch(name: jamf_manual_install_policy_name, cnx: jamf_cnx)
593
+ else
594
+ return if deleting?
595
+
596
+ create_jamf_manual_install_policy
597
+ end
598
+ end
599
+
600
+ # The manual install policy is always scoped to all computers, with
601
+ # exclusions
602
+ #
603
+ # The policy has a custom trigger, or can be installed via self service
604
+ #
605
+ #########################
606
+ def create_jamf_manual_install_policy
607
+ progress "Jamf: Creating Manual Install Policy: #{jamf_manual_install_policy_name}", log: :info
608
+
609
+ pol = Jamf::Policy.create name: jamf_manual_install_policy_name, cnx: jamf_cnx
610
+ configure_jamf_manual_install_policy(pol)
611
+ pol.save
612
+ pol
613
+ end
614
+
615
+ # repair the manual-install policy only
616
+ #############################
617
+ def repair_jamf_manual_install_policy
618
+ pol = jamf_manual_install_policy
619
+ progress "Jamf: Repairing Manual Install Policy '#{jamf_manual_install_policy_name}'", log: :info
620
+ configure_jamf_manual_install_policy(pol)
621
+ pol.save
622
+ end
623
+
624
+ # Configure the given policy as the manual-install policy for this version
625
+ # @param pol [Jamf::Policy] the policy to configure
626
+ ##########################
627
+ def configure_jamf_manual_install_policy(pol)
628
+ pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
629
+ pol.set_trigger_event :checkin, false
630
+ pol.set_trigger_event :custom, jamf_manual_install_trigger
631
+ pol.frequency = :ongoing
632
+ pol.recon = false
633
+
634
+ pol.package_names.each { |pkg_name| pol.remove_package pkg_name }
635
+ pol.add_package jamf_pkg_name
636
+
637
+ set_policy_to_all_targets pol
638
+ set_policy_exclusions pol
639
+
640
+ # These policies shouldn't be in ssvc
641
+ # only the title's jamf_manual_install_released_policy is
642
+ pol.remove_from_self_service if pol.in_self_service?
643
+ pol.enable
644
+ end
645
+
646
+ # @return [String] the URL for the Jamf Pro Policy that does manual installs of this version
647
+ ######################
648
+ def jamf_manual_install_policy_url
649
+ return @jamf_manual_install_policy_url if @jamf_manual_install_policy_url
650
+
651
+ pol_id = Jamf::Policy.valid_id jamf_manual_install_policy_name, cnx: jamf_cnx
652
+ return unless pol_id
653
+
654
+ @jamf_manual_install_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
655
+ end
656
+
657
+ ####### The Jamf Installed Group
658
+ ###########################################
659
+ ###########################################
660
+
661
+ # @return [Boolean] does the jamf_installed_group exist?
662
+ #########################
663
+ def jamf_installed_group_exist?
664
+ Jamf::ComputerGroup.all_names(cnx: jamf_cnx).include? jamf_installed_group_name
665
+ end
666
+
667
+ # Create or fetch the smart group of macs with this version installed
668
+ # If we are deleting and it doesn't exist, return nil
669
+ #
670
+ # @return [Jamf::ComputerGroup] the smart group.
671
+ #########################
672
+ def jamf_installed_group
673
+ return @jamf_installed_group if @jamf_installed_group
674
+
675
+ if jamf_installed_group_exist?
676
+ @jamf_installed_group = Jamf::ComputerGroup.fetch(
677
+ name: jamf_installed_group_name,
678
+ cnx: jamf_cnx
679
+ )
680
+ else
681
+ return if deleting?
682
+ # don't create unless there's been a re-upload of the pkg
683
+ return unless reupload_date
684
+
685
+ create_jamf_installed_group
686
+ end
687
+ @jamf_installed_group
688
+ end
689
+
690
+ # Create the smart group of macs with this version installed
691
+ #
692
+ # @return [Jamf::ComputerGroup] the smart group.
693
+ #########################
694
+ def create_jamf_installed_group
695
+ progress "Jamf: Creating smart group '#{jamf_installed_group_name}'", log: :info
696
+
697
+ @jamf_installed_group = Jamf::ComputerGroup.create(
698
+ name: jamf_installed_group_name,
699
+ type: :smart,
700
+ cnx: jamf_cnx
701
+ )
702
+ configure_jamf_installed_group @jamf_installed_group
703
+ @jamf_installed_group.save
704
+ @jamf_installed_group
705
+ end
706
+
707
+ # Reset the configuration of the jamf_installed_group
708
+ # but only if there's been a re-upload of the pkg
709
+ #########################
710
+ def repair_jamf_installed_group
711
+ return unless reupload_date
712
+
713
+ progress "Jamf: Repairing smart group '#{jamf_installed_group_name}'", log: :info
714
+
715
+ configure_jamf_installed_group jamf_installed_group
716
+ jamf_installed_group.save
717
+ end
718
+
719
+ # Set the configuration of the given smart group
720
+ # as needed for the jamf_installed_group
721
+ # @param grp [Jamf::ComputerGroup] the group to configure
722
+ #
723
+ # @return [void]
724
+ #########################
725
+ def configure_jamf_installed_group(grp)
726
+ progress "Jamf: Setting criteria for smart group '#{grp.name}'", log: :info
727
+ grp.criteria = Jamf::Criteriable::Criteria.new(jamf_installed_group_criteria)
728
+ end
729
+
730
+ # The criteria for the smart group in Jamf that contains all Macs
731
+ # with this version of this title installed
732
+ #
733
+ # If we have, or are about to update to, a version_script (EA) then use it,
734
+ # otherwise use the app_name and app_bundle_id.
735
+ #
736
+ # @return [Array<Jamf::Criteriable::Criterion>]
737
+ ###################################
738
+ def jamf_installed_group_criteria
739
+ # does this title use an app bundle?
740
+ if title_object.app_name
741
+ [
742
+ Jamf::Criteriable::Criterion.new(
743
+ and_or: :and,
744
+ name: 'Application Title',
745
+ search_type: 'is',
746
+ value: title_object.app_name
747
+ ),
748
+
749
+ Jamf::Criteriable::Criterion.new(
750
+ and_or: :and,
751
+ name: 'Application Bundle ID',
752
+ search_type: 'is',
753
+ value: title_object.app_bundle_id
754
+ ),
755
+
756
+ Jamf::Criteriable::Criterion.new(
757
+ and_or: :and,
758
+ name: 'Application Version',
759
+ search_type: 'is',
760
+ value: version
761
+ )
762
+ ]
763
+
764
+ # if not, it must have a version script
765
+ elsif title_object.version_script
766
+ [
767
+ Jamf::Criteriable::Criterion.new(
768
+ and_or: :and,
769
+ name: title_object.jamf_normal_ea_name,
770
+ search_type: 'is',
771
+ value: version
772
+ )
773
+ ]
774
+
775
+ else
776
+ raise Xolo::Core::Exceptions::InvalidDataError, "Title #{title} has neither a version_script nor a defined app bundle."
777
+ end
778
+ end
779
+
780
+ #########################
781
+ def jamf_installed_group_url
782
+ return @jamf_installed_group_url if @jamf_installed_group_url
783
+
784
+ gr_id = Jamf::ComputerGroup.valid_id jamf_installed_group_name, cnx: jamf_cnx
785
+ return unless gr_id
786
+
787
+ @jamf_installed_group_url = "#{jamf_gui_url}/smartComputerGroups.html?id=#{gr_id}&o=r"
788
+ end
789
+
790
+ ####### The Jamf Auto Re-Install Policy
791
+ ###########################################
792
+ ###########################################
793
+
794
+ # @return [Boolean] does the jamf_auto_reinstall_policy exist?
795
+ ##########################
796
+ def jamf_auto_reinstall_policy_exist?
797
+ Jamf::Policy.all_names(cnx: jamf_cnx).include? jamf_auto_reinstall_policy_name
798
+ end
799
+
800
+ # Create or fetch the auto re-install policy for this version
801
+ # If we are deleting and it doesn't exist, return nil.
802
+ #
803
+ # @return [Jamf::Policy] The auto-install-policy for this version, if it exists
804
+ ##########################
805
+ def jamf_auto_reinstall_policy
806
+ @jamf_auto_reinstall_policy ||=
807
+ if jamf_auto_reinstall_policy_exist?
808
+ Jamf::Policy.fetch(name: jamf_auto_reinstall_policy_name, cnx: jamf_cnx)
809
+ else
810
+ return if deleting?
811
+ # don't create unless there's been a re-upload of the pkg
812
+ return unless reupload_date
813
+
814
+ create_jamf_auto_reinstall_policy
815
+ end
816
+ end
817
+
818
+ # The auto rionstall policy, for when a pkg is re-uploaded for this version.
819
+ # @return [Jamf::Policy] the auto install policy for this version
820
+ #########################
821
+ def create_jamf_auto_reinstall_policy
822
+ progress "Jamf: Creating Auto Re-Install Policy: #{jamf_auto_reinstall_policy_name}", log: :debug
823
+ pol = Jamf::Policy.create name: jamf_auto_reinstall_policy_name, cnx: jamf_cnx
824
+ configure_jamf_auto_reinstall_policy(pol)
825
+ pol.save
826
+ pol
827
+ end
828
+
829
+ # Reset the configuration of the jamf_installed_group
830
+ #########################
831
+ def repair_jamf_auto_reinstall_policy
832
+ return unless reupload_date
833
+
834
+ progress "Jamf: Repairing Auto Re-Install Policy: #{jamf_auto_reinstall_policy_name}", log: :debug
835
+ configure_jamf_auto_reinstall_policy jamf_auto_reinstall_policy
836
+ end
837
+
838
+ # Set the proper config for the auto reinstall policy
839
+ # @param pol [Jamf::Policy] the policy to configure
840
+ ######################
841
+ def configure_jamf_auto_reinstall_policy(pol)
842
+ pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
843
+ pol.set_trigger_event :checkin, true
844
+ pol.set_trigger_event :custom, Xolo::BLANK
845
+ pol.frequency = :once_per_computer
846
+ pol.recon = false
847
+ pol.retry_event = :checkin
848
+ pol.retry_attempts = 5
849
+ pol.scope.set_targets :computer_groups, [jamf_installed_group_name]
850
+
851
+ # exclusions are for always
852
+ set_policy_exclusions pol
853
+
854
+ pol.package_names.each { |pkg_name| pol.remove_package pkg_name }
855
+ pol.add_package jamf_pkg_name
856
+
857
+ # NOTE: this policy is not enabled by default - it will be enabled
858
+ # if/when the pkg for the policy is re-uploaded
859
+ pol.enable if reupload_date.is_a? Time
860
+ end
861
+
862
+ # @return [String] the URL for the Jamf Pro Policy that does auto reinstalls of this version
863
+ ######################
864
+ def jamf_auto_reinstall_policy_url
865
+ return @jamf_auto_reinstall_policy_url if @jamf_auto_reinstall_policy_url
866
+
867
+ pol_id = Jamf::Policy.valid_id jamf_auto_reinstall_policy_name, cnx: jamf_cnx
868
+ return unless pol_id
869
+
870
+ @jamf_auto_reinstall_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
871
+ end
872
+
873
+ # This will start a thread
874
+ # that will wait some period of time (to allow for pkg uploads
875
+ # to complete) before enabling and flushing the logs for the reinstall policy.
876
+ # This will make all macs with this version installed get it re-installed.
877
+ # @return [void]
878
+ def wait_to_enable_reinstall_policy
879
+ return if @enable_reinstall_policy_thread&.alive?
880
+ return unless reupload_date
881
+
882
+ # TODO: some setting to determine how long to wait?
883
+ # - If uploading via the Jamf API, we need to give it time
884
+ # to then upload the file to the cloud distribution point
885
+ # - If uploading via a custom tool, we need to give that
886
+ # tool time to re-upload to wherever it uploads to
887
+ # - May need to wait for other non-jamf/non-xolo processes
888
+ # to sync the package to other distribution points. This
889
+ # might be very site-specific.
890
+
891
+ # For now, we wait 15 minutes.
892
+ wait_time = JAMF_AUTO_REINSTALL_WAIT_SECS
893
+
894
+ @enable_reinstall_policy_thread = Thread.new do
895
+ log_debug "Jamf: Starting enable_reinstall_policy_thread: waiting #{wait_time} seconds before enabling reinstall policy for version #{version} of title #{title}"
896
+ sleep wait_time
897
+
898
+ log_debug "Jamf: enable_reinstall_policy_thread: enabling and flushing logs for reinstall policy for version #{version} of title #{title}"
899
+
900
+ pol = jamf_auto_reinstall_policy
901
+ pol.enable
902
+ pol.flush_logs
903
+ pol.save
904
+ end
905
+ @enable_reinstall_policy_thread.name = "enable_reinstall_policy_thread-#{title}-#{version}"
906
+ end
907
+
908
+ ####### The Jamf Patch Policy
909
+ ###########################################
910
+ ###########################################
911
+
912
+ # @return [Boolean] does the jamf_patch_policy exist?
913
+ ###########################
914
+ def jamf_patch_policy_exist?
915
+ Jamf::PatchPolicy.all_names(:refresh, cnx: jamf_cnx).include? jamf_patch_policy_name
916
+ end
917
+
918
+ # Fetch or create the patch policy for this version
919
+ # If we are deleting and it doesn't exist, return nil.
920
+ # @return [Jamf::PatchPolicy, nil] The patch policy for this version, if it exists
921
+ ##########################
922
+ def jamf_patch_policy
923
+ @jamf_patch_policy ||=
924
+ if jamf_patch_policy_exist?
925
+ Jamf::PatchPolicy.fetch(name: jamf_patch_policy_name, cnx: jamf_cnx)
926
+ else
927
+ return if deleting?
928
+
929
+ create_jamf_patch_policy
930
+ end
931
+ end
932
+
933
+ # @return [Jamf::PatchPolicy] The xolo patch policy for this version
934
+ #########################
935
+ def create_jamf_patch_policy
936
+ progress "Jamf: Creating Patch Policy for Version '#{version}' of Title '#{title}'.", log: :info
937
+
938
+ # TODO: decide how many patch policies - see comments at top
939
+ # Probably one: for pilots initially and then rescoped to all for release
940
+ #
941
+ # TODO: How to set these, and should they be settable
942
+ # at the Xolo::Title or Xolo::Version level?
943
+ #
944
+ # allow downgrade? No, to start with.
945
+ # When a version is released, IF we are rolling back, then this will be set.
946
+ # This is to be set only on the current release and only when it was a rollback.
947
+ #
948
+ # patch_unknown_versions... yes?
949
+ #
950
+ # if not in ssvc:
951
+ # - grace period?
952
+ # - update warning Message and Subject
953
+ #
954
+ # if in ssvc:
955
+ # - any way to use existing icon?
956
+ # - use title desc... do we want a version desc??
957
+ # - notifications? Message and Subject? SSvc only, Notif Ctr?
958
+ # - deadline and grace period message and subbject
959
+
960
+ ppol = Jamf::PatchPolicy.create(
961
+ cnx: jamf_cnx,
962
+ name: jamf_patch_policy_name,
963
+ patch_title: title_object.jamf_patch_title.id,
964
+ target_version: version
965
+ )
966
+
967
+ # when first creating a patch policy, its status is always
968
+ # 'pilot' so the scope targets are the pilot groups, if any.
969
+ # When the version is released, the patch policy will be
970
+ # rescoped to all targets (limited by eligibility)
971
+ set_policy_pilot_groups ppol
972
+
973
+ # exclusions are for always
974
+ set_policy_exclusions ppol
975
+
976
+ # This will be set to true as needed if
977
+ # a rollback is being done
978
+ ppol.allow_downgrade = false
979
+
980
+ # This should always be false, so that
981
+ # we don't accidentally downgrade non-xolo test installs,
982
+ # or server-pushed updates (like with commvault or cisco VPN)
983
+ ppol.patch_unknown = false
984
+
985
+ ppol.enable
986
+
987
+ ppol.save
988
+
989
+ # refetch it rather than using the one we just created
990
+ Jamf::PatchPolicy.fetch(name: jamf_patch_policy_name, cnx: jamf_cnx)
991
+ end
992
+
993
+ # repair the patch policy only
994
+ #############################
995
+ def repair_jamf_patch_policy
996
+ progress "Jamf: Repairing Patch Policy '#{jamf_patch_policy_name}'", log: :info
997
+ assign_pkg_to_patch_in_jamf
998
+
999
+ ppol = jamf_patch_policy
1000
+ ppol.name = jamf_patch_policy_name
1001
+ ppol.target_version = version
1002
+
1003
+ # This should always be false, so that
1004
+ # we don't accidentally downgrade non-xolo test installs,
1005
+ # or server-pushed updates (like with commvault or cisco VPN)
1006
+ ppol.patch_unknown = false
1007
+
1008
+ if pilot?
1009
+ set_policy_pilot_groups ppol
1010
+ else
1011
+ ppol.scope.set_all_targets
1012
+ end
1013
+
1014
+ # exclusions are for always
1015
+ set_policy_exclusions ppol
1016
+
1017
+ if pilot? || released?
1018
+ ppol.enable
1019
+ else
1020
+ ppol.disable
1021
+ end
1022
+
1023
+ ppol.save
1024
+ end
1025
+
1026
+ # @return [String] the URL for the Jamf Pro Patch Policy that updates to this version
1027
+ ######################
1028
+ def jamf_patch_policy_url
1029
+ return @jamf_patch_policy_url if @jamf_patch_policy_url
1030
+
1031
+ title_id = title_object.jamf_patch_title_id
1032
+
1033
+ pol_id = Jamf::PatchPolicy.valid_id jamf_patch_policy_name, cnx: jamf_cnx
1034
+ return unless pol_id
1035
+
1036
+ @jamf_manual_install_policy_url = "#{jamf_gui_url}/patchDeployment.html?softwareTitleId=#{title_id}&id=#{pol_id}&o=r"
1037
+ end
1038
+
1039
+ ####### General Policy Handling
1040
+ ###########################################
1041
+ ###########################################
1042
+
1043
+ # set target groups in a pilot [patch] policy object's scope
1044
+ # REMEMBER TO SAVE THE POLICY LATER
1045
+ #
1046
+ # @param pol [Jamf::Policy, Jamf::PatchPolicy]
1047
+ # @return [void]
1048
+ ############################
1049
+ def set_policy_pilot_groups(pol)
1050
+ pilots = pilot_groups_to_use
1051
+ pilots ||= []
1052
+ log_debug "Jamf: setting pilot scope targets for #{pol.class} '#{pol.name}' to: #{pilots.join ', '}"
1053
+
1054
+ pol.scope.set_targets :computer_groups, pilots
1055
+ end
1056
+
1057
+ # Set a policy to be scoped to all targets
1058
+ # REMEMBER TO SAVE THE POLICY LATER
1059
+ #
1060
+ # @param pol [Jamf::Policy, Jamf::PatchPolicy]
1061
+ # @return [void]
1062
+ ############################
1063
+ def set_policy_to_all_targets(pol)
1064
+ log_debug "Jamf: setting scope target for #{pol.class} '#{pol.name}' to all computers"
1065
+ pol.scope.set_all_targets
1066
+ end
1067
+
1068
+ # set target groups in a non=pilot [patch] policy object's scope
1069
+ # REMEMBER TO SAVE THE POLICY LATER
1070
+ #
1071
+ # @param pol [Jamf::Policy, Jamf::PatchPolicy]
1072
+ # @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
1073
+ # if nil, we'll instantiate it now
1074
+ # @return [void]
1075
+ ############################
1076
+ def set_policy_release_groups(pol, ttl_obj: nil)
1077
+ ttl_obj ||= title_object
1078
+ targets = release_groups_to_use(ttl_obj: ttl_obj) || []
1079
+
1080
+ log_debug "Jamf: setting release scope targets for #{pol.class} '#{pol.name}' to: #{targets.join ', '}"
1081
+
1082
+ if targets.include? Xolo::TARGET_ALL
1083
+ pol.scope.set_all_targets
1084
+ else
1085
+ pol.scope.set_targets :computer_groups, targets
1086
+ end
1087
+ end
1088
+
1089
+ # set excluded groups in a [patch] policy object's scope
1090
+ # REMEMBER TO SAVE THE POLICY LATER
1091
+ #
1092
+ # This applies more nuance to the 'excluded_groups_to_use' depending on
1093
+ # the policy in question. E.g. manual-install policy should not
1094
+ # have the installed-group excluded, to allow re-installs
1095
+ #
1096
+ # @param pol [Jamf::Policy, Jamf::PatchPolicy]
1097
+ # @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
1098
+ # if nil, we'll instantiate it now
1099
+ ############################
1100
+ def set_policy_exclusions(pol, ttl_obj: nil)
1101
+ ttl_obj ||= title_object
1102
+ # dup, so when we add the installed group below, we don't
1103
+ # keep that for future calls to this method.
1104
+ exclusions = excluded_groups_to_use(ttl_obj: ttl_obj).dup
1105
+ exclusions ||= []
1106
+
1107
+ # the initial auto-install policies must also exclude any mac with the title
1108
+ # already installed
1109
+ #
1110
+ # But the manual-install policy and the auto-reinstall should never exclude it - so that
1111
+ # it can be manually installed or automatically re-installed whenever needed
1112
+ if pol.is_a?(Jamf::Policy) && pol.name == jamf_auto_install_policy_name
1113
+ # calling ttl_obj.jamf_installed_group will create the group if needed
1114
+ exclusions << ttl_obj.jamf_installed_group_name
1115
+ end
1116
+
1117
+ log_debug "Jamf: updating exclusions for #{pol.class} '#{pol.name}' to: #{exclusions.join ', '}"
1118
+
1119
+ exclusions.uniq!
1120
+ pol.scope.set_exclusions :computer_groups, exclusions
1121
+ end
1122
+
1123
+ # Disable the auto-install and patch policies for this version when it
1124
+ # is deprecated or skipped
1125
+ #
1126
+ # Leave the manual install policy active, but remove it from self-service
1127
+ #
1128
+ # @param reason [Symbol] :deprecated or :skipped
1129
+ #
1130
+ # @return [void]
1131
+ #########################
1132
+ def disable_policies_for_deprecation_or_skipping(reason)
1133
+ progress "Jamf: Disabling auto-install policy for #{reason} version '#{version}'"
1134
+ pol = jamf_auto_install_policy
1135
+ pol.disable
1136
+ pol.save
1137
+
1138
+ # don't disable the auto-reinstall policy - it may be needed
1139
+ # for any re-uploads of the pkg for this version, even if deprecated/skipped
1140
+
1141
+ progress "Jamf: Disabling patch policy for #{reason} version '#{version}'"
1142
+ ppol = jamf_patch_policy
1143
+ ppol.disable
1144
+ # ensure patch policy is NOT set to 'allow downgrade'
1145
+ ppol.allow_downgrade = false
1146
+ ppol.save
1147
+ end
1148
+
1149
+ # reset all the policies for this version to pilot
1150
+ #
1151
+ # @return [void]
1152
+ ######################
1153
+ def reset_policies_to_pilot
1154
+ # set scope targets of auto-install policy to pilot-groups and re-enable
1155
+ msg = "Jamf: Version '#{version}': Setting scope targets of auto-install policy to pilot_groups: #{pilot_groups_to_use.join(', ')}"
1156
+ progress msg, log: :info
1157
+
1158
+ jamf_auto_install_policy.scope.set_targets :computer_groups, pilot_groups_to_use
1159
+ jamf_auto_install_policy.enable
1160
+ jamf_auto_install_policy.save
1161
+
1162
+ msg = "Jamf: Version '#{version}': Setting scope targets of patch policy to pilot_groups"
1163
+ progress msg, log: :info
1164
+
1165
+ # set scope targets of patch policy to pilot-groups and re-enable
1166
+ jamf_patch_policy.scope.set_targets :computer_groups, pilot_groups_to_use
1167
+ # ensure patch policy is NOT set to 'allow downgrade'
1168
+ jamf_patch_policy.allow_downgrade = false
1169
+ jamf_patch_policy.enable
1170
+ jamf_patch_policy.save
1171
+ end
1172
+
1173
+ # Update all the pilot_groups policy scopes for this version when
1174
+ # either the title or version has changed them
1175
+ #
1176
+ # Nothing to do if the version isn't currently in :pilot status
1177
+ #
1178
+ # @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
1179
+ # if nil, we'll instantiate it now
1180
+ #########################
1181
+ def update_pilot_groups
1182
+ # nothing unless we're in pilot
1183
+ return unless status == Xolo::Server::Version::STATUS_PILOT
1184
+
1185
+ # - no changes to the manual install policy: scope-target is all
1186
+
1187
+ # - update the auto install policy
1188
+ progress "Jamf: Updating pilot groups for Auto Install Policy '#{jamf_auto_install_policy_name}'."
1189
+
1190
+ pol = jamf_auto_install_policy
1191
+
1192
+ set_policy_pilot_groups(pol)
1193
+ pol.save
1194
+
1195
+ # - update the patch policy
1196
+ progress "Jamf: Updating pilot groups for Patch Policy '#{jamf_patch_policy_name}'."
1197
+
1198
+ pol = jamf_patch_policy
1199
+
1200
+ set_policy_pilot_groups(pol)
1201
+ pol.save
1202
+ end
1203
+
1204
+ # Update all the release_groups policy scopes for this version when
1205
+ # either the title or version has changed them
1206
+ #
1207
+ # Nothing to do if the version is currently in pending or pilot status
1208
+ #
1209
+ # @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
1210
+ # if nil, we'll instantiate it now
1211
+ #########################
1212
+ def update_release_groups(ttl_obj: nil)
1213
+ return unless status == Xolo::Server::Version::STATUS_RELEASED
1214
+
1215
+ # - no changes to the manual install policy: scope-target is all
1216
+
1217
+ # - update the auto-install policy
1218
+ pol = jamf_auto_install_policy
1219
+ return unless pol
1220
+
1221
+ set_policy_release_groups(pol, ttl_obj: ttl_obj)
1222
+ pol.save
1223
+ progress "Jamf: updated release groups for Auto Install Policy '#{jamf_auto_install_policy_name}'.",
1224
+ log: :info
1225
+
1226
+ # - no changes to the patch policy: scope-target is all once released
1227
+ end
1228
+
1229
+ # Update all the excluded_groups policy scopes for this version when
1230
+ # either the title or version has changed them
1231
+ #
1232
+ # Applies regardless of status
1233
+ #
1234
+ # @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
1235
+ # if nil, we'll instantiate it now
1236
+ #########################
1237
+ def update_excluded_groups(ttl_obj: nil)
1238
+ log_debug "Updating Excluded Groups for Version '#{version}' of Title '#{title}'"
1239
+
1240
+ # - update the manual install policy
1241
+ pol = jamf_manual_install_policy
1242
+ if pol
1243
+ progress "Jamf: Updating excluded groups for Manual Install Policy '#{jamf_auto_install_policy_name}'."
1244
+ set_policy_exclusions(pol, ttl_obj: ttl_obj)
1245
+ pol.save
1246
+ end
1247
+
1248
+ # - update the auto install policy
1249
+ pol = jamf_auto_install_policy
1250
+ if pol
1251
+ progress "Jamf: Updating excluded groups for Auto Install Policy '#{jamf_auto_install_policy_name}'."
1252
+ set_policy_exclusions(pol, ttl_obj: ttl_obj)
1253
+ pol.save
1254
+ end
1255
+
1256
+ # - update the auto reinstall policy, but only if it exists
1257
+ if jamf_auto_reinstall_policy_exist?
1258
+ pol = jamf_auto_reinstall_policy
1259
+ if pol
1260
+ progress "Jamf: Updating excluded groups for Auto ReInstall Policy '#{jamf_auto_reinstall_policy_name}'."
1261
+ set_policy_exclusions(pol, ttl_obj: ttl_obj)
1262
+ pol.save
1263
+ end
1264
+ end
1265
+
1266
+ # - update the patch policy
1267
+ pol = jamf_patch_policy
1268
+ return unless pol
1269
+
1270
+ progress "Jamf: Updating exccluded groups for Patch Policy '#{jamf_patch_policy_name}'."
1271
+ set_policy_exclusions(pol, ttl_obj: ttl_obj)
1272
+ pol.save
1273
+ end
1274
+
1275
+ ####### General Jamf Patch Handling
1276
+ ###########################################
1277
+ ###########################################
1278
+
1279
+ # @return [Jamf::PatchTitle::Version] The Jamf::PatchTitle::Version for this
1280
+ # Xolo version
1281
+ #####################
1282
+ def jamf_patch_version
1283
+ return @jamf_patch_version if @jamf_patch_version
1284
+
1285
+ # NOTE: in the line below, use the title_object's call to #jamf_patch_title
1286
+ # because that will cache the Jamf::PatchTitle instance, and we need to
1287
+ # use it to save changes to its Versions.
1288
+ # Using the class method won't cache the instance we will need in the
1289
+ # future.
1290
+ @jamf_patch_version = title_object.jamf_patch_title.versions[version]
1291
+ return @jamf_patch_version if @jamf_patch_version
1292
+
1293
+ # TODO: wait for it to appear when adding?
1294
+ msg = "Jamf: Version '#{version}' of Title '#{title}' is not visible in Jamf. Is the Patch enabled in the Title Editor?"
1295
+ log_error msg
1296
+ raise Xolo::NoSuchItemError, msg
1297
+ end
1298
+
1299
+ # Wait until the version is visible from the title editor
1300
+ # then assign the pkg to it in Jamf Patch,
1301
+ # and create the patch policy.
1302
+ #
1303
+ # Do this in a thread so the xadm user doesn't wait up to ?? minutes.
1304
+ #
1305
+ # @return [void]
1306
+ #########################
1307
+ def activate_patch_version_in_jamf
1308
+ # don't do this if there's already one running for this instance
1309
+ if @activate_patch_version_thread&.alive?
1310
+ log_debug "Jamf: activate_patch_version_thread already running. Caller: #{caller_locations.first}"
1311
+ return
1312
+ end
1313
+
1314
+ msg = "Jamf: Will assign Jamf pkg '#{jamf_pkg_name}' and create the patch policy when this version becomes visible to Jamf Pro from the Title Editor."
1315
+ progress msg, log: :debug
1316
+
1317
+ @activate_patch_version_thread = Thread.new do
1318
+ log_debug "Jamf: Starting activate_patch_version_thread waiting for version #{version} of title #{title} to become visible from the title editor"
1319
+
1320
+ start_time = Time.now
1321
+ max_time = start_time + Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR
1322
+ start_time = start_time.strftime '%F %T'
1323
+
1324
+ did_it = false
1325
+
1326
+ while Time.now < max_time
1327
+ sleep 15
1328
+ log_debug "Jamf: checking for version #{version} of title #{title} to become visible from the title editor since #{start_time}"
1329
+
1330
+ # check for the existence of the jamf_patch_title every time, since it might have gone away
1331
+ # if the title was deleted while this was happening.
1332
+ next unless title_object.jamf_patch_title(refresh: true) && title_object.jamf_patch_title.versions.key?(version)
1333
+
1334
+ did_it = true
1335
+ break
1336
+ end
1337
+
1338
+ if did_it
1339
+ assign_pkg_to_patch_in_jamf
1340
+ # give jamf a moment to catch up and refresh the patch title
1341
+ # so we see the pkg has been assigned
1342
+ sleep 2
1343
+ title_object.jamf_patch_title(refresh: true)
1344
+
1345
+ create_jamf_patch_policy
1346
+ msg = "Jamf: Version '#{version}' of title '#{title}' is now visible in Jamf Pro. Package assigned and Patch policy created."
1347
+ log_info msg, alert: true
1348
+ else
1349
+ msg = "Jamf: ERROR: Version '#{version}' of title '#{title}' has not become visible from the Title Editor in over #{Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR} seconds. The package has not been assigned, and no patch policy was created."
1350
+ log_error msg, alert: true
1351
+ end
1352
+ end # thread
1353
+ @activate_patch_version_thread.name = "activate_patch_version_thread-#{title}-#{version}"
1354
+ end
1355
+
1356
+ # Assign the Package to the Jamf::PatchTitle::Version for this Xolo version.
1357
+ # This 'activates' the version in Jamf Patch, and must happen before
1358
+ # patch policies can be created
1359
+ #
1360
+ # Jamf::PatchTitle::Version objects are contained in the matching
1361
+ # Jamf::PatchTitle, and to make or save changes, we have to fetch the title,
1362
+ # update the version, and save the title.
1363
+ #
1364
+ # NOTE: This can't happen until Jamf see's the version in the title editor
1365
+ # otherwise you'll get an error. The methods that call this should ensure
1366
+ # the version is visible.
1367
+ #
1368
+ # @return [void]
1369
+ ########################################
1370
+ def assign_pkg_to_patch_in_jamf
1371
+ log_info "Jamf: Assigning package '#{jamf_pkg_name}' to patch version '#{version}' of title '#{title}'"
1372
+
1373
+ jamf_patch_version.package = jamf_pkg_name
1374
+ title_object.jamf_patch_title.save
1375
+ end
1376
+
1377
+ # Get the patch report for this version
1378
+ # @return [Arrah<Hash>] Data for each computer with this version of this title installed
1379
+ ######################
1380
+ def patch_report
1381
+ title_object.patch_report vers: version
1382
+ end
1383
+
1384
+ ####### MDM Deployment
1385
+ ###########################################
1386
+ ###########################################
1387
+
1388
+ # Install this version on a one or more computers via MDM.
1389
+ #
1390
+ # @param targets [Hash ] With the following keys
1391
+ # - computers: [Array<String, Integer>] The computer identifiers to install on.
1392
+ # Identifiers are either serial numbers, names, or Jamf IDs.
1393
+ # - groups: [Array<String, Integer>] The names or ids of computer groups to install on.
1394
+ #
1395
+ # @return [Hash] The results of the install with the following keys
1396
+ # - removals: [Array<Hash>] { device: <String>, group: <InteStringger>, reason: <String> }
1397
+ # - queuedCommands: [Array<Hash>] { device: <String>, commandUuid: <String> }
1398
+ # - errors: [Array<Hash>] { device: <String>, group: <Integer>, reason: <String> }
1399
+ #
1400
+ def deploy_via_mdm(targets)
1401
+ unless dist_pkg
1402
+ raise Xolo::UnsupportedError,
1403
+ 'MDM deployment is not supported for this version, it is not a Distribution Package.'
1404
+ end
1405
+
1406
+ all_targets = targets[:computers] || []
1407
+ removals = []
1408
+
1409
+ # expand groups into computers,
1410
+ all_targets += expand_groups_for_deploy(targets[:groups], removals) if targets[:groups]
1411
+
1412
+ # remove duplicates
1413
+ all_targets.uniq!
1414
+
1415
+ # remove invalid computers, after this all_targets will be valid computer ids
1416
+ remove_invalid_computers_for_deploy(all_targets, removals)
1417
+
1418
+ # remove members of excluded groups from the list of targets
1419
+ remove_exclusions_from_deploy(all_targets, removals)
1420
+
1421
+ if all_targets.empty?
1422
+ log_info "Jamf: No valid computers to deploy to for version '#{version}' of title '#{title}'."
1423
+ queued_cmds = []
1424
+ deploy_errs = []
1425
+
1426
+ else
1427
+ # deploy the package to the computers
1428
+ jamf_package.deploy_via_mdm computer_ids: all_targets
1429
+ # convert ids to names for the response
1430
+ comp_ids_to_names = Jamf::Computer.map_all(:id, to: :name, cnx: jamf_cnx)
1431
+
1432
+ queued_cmds = jamf_package.deploy_response[:queuedCommands].map do |qc|
1433
+ { device: comp_ids_to_names[qc[:device]], commandUuid: qc[:commandUuid] }
1434
+ end
1435
+
1436
+ deploy_errs = jamf_package.deploy_response[:errors].map do |err|
1437
+ { device: comp_ids_to_names[err[:device]], reason: err[:reason] }
1438
+ end
1439
+
1440
+ log_info "Jamf: Deployed version '#{version}' of title '#{title}' to #{all_targets.size} computers via MDM"
1441
+
1442
+ end
1443
+
1444
+ removals.each { |r| log_info "Jamf: Removal #{r}" }
1445
+ queued_cmds.each { |qc| log_info "Jamf: Queued Command #{qc}" }
1446
+ deploy_errs.each { |err| log_info "Jamf: Error #{err}" }
1447
+
1448
+ {
1449
+ removals: removals,
1450
+ queuedCommands: queued_cmds,
1451
+ errors: deploy_errs
1452
+ }
1453
+ end
1454
+
1455
+ # expand computer groups given for deploy_via_mdm
1456
+ #
1457
+ # @param groups [Array<String, Integer>] The names or ids of computer groups to install on.
1458
+ # @param removals [Array<Hash>] The groups that are not valid, for reporting back to the caller
1459
+ #
1460
+ # @return [Array<Integer>] The ids of the computers in the groups
1461
+ #########################
1462
+ def expand_groups_for_deploy(groups, removals)
1463
+ log_debug "Expanding group targets for MDM deployment of title '#{title}', version '#{version}'"
1464
+
1465
+ computers = []
1466
+ groups.each do |g|
1467
+ gid = Jamf::ComputerGroup.valid_id g, cnx: jamf_cnx
1468
+ if gid
1469
+ jgroup = Jamf::ComputerGroup.fetch id: gid, cnx: jamf_cnx
1470
+
1471
+ if excluded_groups_to_use.include? jgroup.name
1472
+ log_debug "Jamf: Group '#{jgroup.name}' is in the excluded groups list. Removing."
1473
+ removals << { device: nil, group: g, reason: "Group '#{jgroup.name}' is in the excluded groups list" }
1474
+ next
1475
+ end
1476
+ log_debug "Jamf: Adding computers from group '#{jgroup.name}' to deployment targets"
1477
+ computers += jgroup.member_ids
1478
+ else
1479
+ log_debug "Jamf: Group '#{g}' not found in Jamf Pro. Removing."
1480
+ removals << { device: nil, group: g, reason: 'Group not found in Jamf Pro' }
1481
+ end
1482
+ end
1483
+ computers
1484
+ end
1485
+
1486
+ # remove invalid computers from the list of targets for deploy_via_mdm
1487
+ #
1488
+ # @param targets [Array<String, Integer>] The names or ids of computers to install on.
1489
+ # @param removals [Array<Hash>] The computers that are not valid, for reporting back to the caller
1490
+ #
1491
+ # @return [void]
1492
+ #########################
1493
+ def remove_invalid_computers_for_deploy(targets, removals)
1494
+ log_debug "Removing invalid computer targets for MDM deployment of title '#{title}', version '#{version}'"
1495
+
1496
+ targets.map! do |c|
1497
+ id = Jamf::Computer.valid_id c, cnx: jamf_cnx
1498
+ if id
1499
+ id
1500
+ else
1501
+ removals << { device: c, group: nil, reason: 'Computer not found in Jamf Pro' }
1502
+ nil
1503
+ end
1504
+ end.compact!
1505
+ end
1506
+
1507
+ # Remove exclusions from the list of targets for deploy_via_mdm
1508
+ #
1509
+ # @param targets [Array<Integer>] The ids of computers to install on.
1510
+ # @param removals [Array<Hash>] The computers that are not valid, for reporting back to the caller
1511
+ #
1512
+ # @return [void]
1513
+ #########################
1514
+ def remove_exclusions_from_deploy(targets, removals)
1515
+ log_debug "Removing excluded computer targets for MDM deployment of title '#{title}', version '#{version}'"
1516
+
1517
+ excluded_groups_to_use.each do |group|
1518
+ gid = Jamf::ComputerGroup.valid_id group, cnx: jamf_cnx
1519
+ unless gid
1520
+ log_error "Jamf: Excluded group '#{group}' not found in Jamf Pro. Skipping."
1521
+ next
1522
+ end # unless gid
1523
+
1524
+ jgroup = Jamf::ComputerGroup.fetch id: gid, cnx: jamf_cnx
1525
+ jgroup.members.each do |member|
1526
+ next unless targets.include? member[:id]
1527
+
1528
+ log_debug "Jamf: Removing computer '#{member[:name]}' (#{member[:id]}) from deployment targets because it is in excluded group '#{group}'"
1529
+ targets.delete member[:id]
1530
+ removals << { device: member[:name], group: nil, reason: "In excluded group '#{group}'" }
1531
+ end
1532
+ end # excluded_groups_to_use.each
1533
+ end
1534
+
1535
+ end # VersionJamfAccess
1536
+
1537
+ end # Mixins
1538
+
1539
+ end # Server
1540
+
1541
+ end # module Xolo