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,1143 @@
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
+ # frozen_string_literal: true
7
+
8
+ # main module
9
+ module Xolo
10
+
11
+ module Server
12
+
13
+ # A Title in Xolo, as used on the server
14
+ #
15
+ # The code in this file mostly deals with the data on the Xolo server itself, and
16
+ # general methods for manipulating the title.
17
+ #
18
+ # Code for interacting with the Title Editor and Jamf Pro are in the helpers and mixins.
19
+ #
20
+ # NOTE be sure to only instantiate these using the
21
+ # servers 'instantiate_title' method, or else
22
+ # they might not have all the correct innards
23
+ #
24
+ class Title < Xolo::Core::BaseClasses::Title
25
+
26
+ # Mixins
27
+ #############################
28
+ #############################
29
+
30
+ include Xolo::Server::Helpers::JamfPro
31
+ include Xolo::Server::Helpers::TitleEditor
32
+ include Xolo::Server::Helpers::Log
33
+ include Xolo::Server::Helpers::Notification
34
+
35
+ include Xolo::Server::Mixins::Changelog
36
+ include Xolo::Server::Mixins::TitleJamfAccess
37
+ include Xolo::Server::Mixins::TitleTedAccess
38
+
39
+ # Constants
40
+ ######################
41
+ ######################
42
+
43
+ # On the server, xolo titles are represented by directories
44
+ # in this directory, named with the title name.
45
+ #
46
+ # So a title 'foobar' would have a directory
47
+ # (Xolo::Server::DATA_DIR)/titles/foobar/
48
+ # and in there will be a file
49
+ # foobar.json
50
+ # with the data for the Title instance itself
51
+ #
52
+ # Also in there will be a 'versions' dir containing json
53
+ # files for each version of the title.
54
+ # See {Xolo::Server::Version}
55
+ #
56
+ TITLES_DIR = Xolo::Server::DATA_DIR + 'titles'
57
+
58
+ # when creating new titles in the title editor,
59
+ # This is the 'currentVersion', which is required
60
+ # when creating.
61
+ # When the first version/patch is added, the
62
+ # title's value for this will be updated.
63
+ NEW_TITLE_CURRENT_VERSION = '0.0.0'
64
+
65
+ # If a title has a 'version_script'
66
+ # the the contents are stored in the title dir
67
+ # in a file with this name
68
+ VERSION_SCRIPT_FILENAME = 'version-script'
69
+
70
+ # If a title is uninstallable, it will
71
+ # have a script in Jamf, which is also saved in this file
72
+ # on the xolo server.
73
+ UNINSTALL_SCRIPT_FILENAME = 'uninstall-script'
74
+
75
+ # In the TitleEditor, the version script is
76
+ # stored as an Extension Attribute - each title can
77
+ # only have one.
78
+ # and it needs a 'key', which is the name used to indicate the
79
+ # EA in various criteria, and is the EA name in Jamf Patch.
80
+ # The key is this value as a prefix on the title
81
+ # so for title 'foobar', it is 'xolo-foobar'
82
+ # That value is also used as the display name
83
+ TITLE_EDITOR_EA_KEY_PREFIX = Xolo::Server::JAMF_OBJECT_NAME_PFX
84
+
85
+ # The EA from the title editor, which is used in Jamf Patch
86
+ # cannot, unfortunately, be used as a criterion in normal
87
+ # smart groups or advanced searches.
88
+ # Since we need a smart group containing all macs with any
89
+ # version of the title installed, we need a second copy of the
90
+ # EA as a 'normal' EA.
91
+ #
92
+ # (That group is used as an exclusion to any auto-install initial-
93
+ # install policies, so that those policies don't stomp on the matching
94
+ # Patch Policies)
95
+ #
96
+ # The 'duplicate' EA is named the same as the Titled Editor key
97
+ # (see TITLE_EDITOR_EA_KEY_PREFIX) with this suffix added.
98
+ # So for the Title Editor key 'xolo-<title>', we'll also have
99
+ # a matching normal EA called 'xolo-<title>-installed-version'
100
+ JAMF_NORMAL_EA_NAME_SUFFIX = '-installed-version'
101
+
102
+ JAMF_INSTALLED_GROUP_NAME_SUFFIX = '-installed'
103
+ JAMF_FROZEN_GROUP_NAME_SUFFIX = '-frozen'
104
+
105
+ JAMF_UNINSTALL_SUFFIX = '-uninstall'
106
+ JAMF_EXPIRE_SUFFIX = '-expire'
107
+
108
+ # the expire policy will run this client command,
109
+ # appending the title
110
+ # We don't specify a full path so that localized installations
111
+ # will work as long as its in roots default path
112
+ # e.g. /usr/local/bin vs /usr/local/pixar/bin
113
+ CLIENT_EXPIRE_COMMAND = 'xolo expire'
114
+
115
+ # When we are given a Self Service icon for the title,
116
+ # we might not be ready to upload it to jamf, cuz until we
117
+ # have a version to pilot, there's nothing IN jamf.
118
+ # So we always store it locally in this file inside the
119
+ # title dir. The extension from the original file will be
120
+ # appended, e.g. '.png'
121
+ SELF_SERVICE_ICON_FILENAME = 'self-service-icon'
122
+
123
+ # The JPAPI endpoint for Patch Titles.
124
+ #
125
+ # ruby-jss still uses the Classic API for Patch Titles, and won't
126
+ # by migrated to JPAPI until Jamf fully implements all aspects of
127
+ # patch management to JPAPI. As of this writing, that's not the case.
128
+ # But, the JPAPI endpoint for Patch Title Reporting returns more
129
+ # detailed data than the Classic API, so we use it here, and will
130
+ # keep using it as we move forward.
131
+ #
132
+ # This is the top-level endpoint for all patch titles,
133
+ # see JPAPI_PATCH_REPORT_RSRC for the reporting endpoint below it.
134
+ #
135
+ # TODO: Remove this and update relevant methods when ruby-jss
136
+ # is updated to use JPAPI for Patch Titles..
137
+ JPAPI_PATCH_TITLE_RSRC = 'v2/patch-software-title-configurations'
138
+
139
+ # The JPAPI endpoint for patch reporting.
140
+ # The JPAPI_PATCH_TITLE_RSRC is appended with "/<id>/#{JPAPI_PATCH_REPORT_RSRC}"
141
+ # to get the URL for the patch report for a specific title.
142
+ #
143
+ # TODO: Remove this and update relevant methods when ruby-jss
144
+ # is updated to use JPAPI for Patch Titles..
145
+ #
146
+ JPAPI_PATCH_REPORT_RSRC = 'patch-report'
147
+
148
+ SELF_SERVICE_INSTALL_BTN_TEXT = 'Install'
149
+
150
+ # Class Methods
151
+ ######################
152
+ ######################
153
+
154
+ # @return [Array<Pathname>] A list of all known title dirs
155
+ ######################
156
+ def self.title_dirs
157
+ TITLES_DIR.children
158
+ end
159
+
160
+ # @return [Array<String>] A list of all known titles,
161
+ # just the basenames of all the title_dirs
162
+ ######################
163
+ def self.all_titles
164
+ title_dirs.map(&:basename).map(&:to_s)
165
+ end
166
+
167
+ # @return [String] The key and display name of a version script stored
168
+ # in the title editor as the ExtAttr for a given title
169
+ #####################
170
+ def self.ted_ea_key(title)
171
+ "#{TITLE_EDITOR_EA_KEY_PREFIX}#{title}"
172
+ end
173
+
174
+ # @return [String] The display name of a version script as a normal
175
+ # EA in Jamf, which can be used in Smart Groups and Adv Searches.
176
+ #####################
177
+ def self.jamf_normal_ea_name(title)
178
+ "#{ted_ea_key(title)}#{JAMF_NORMAL_EA_NAME_SUFFIX}"
179
+ end
180
+
181
+ # The title dir for a given title on the server,
182
+ # which may or may not exist.
183
+ #
184
+ # @pararm title [String] the title we care about
185
+ #
186
+ # @return [Pathname]
187
+ #####################
188
+ def self.title_dir(title)
189
+ TITLES_DIR + title
190
+ end
191
+
192
+ # The the local JSON file containing the current values
193
+ # for the given title
194
+ #
195
+ # @pararm title [String] the title we care about
196
+ #
197
+ # @return [Pathname]
198
+ #####################
199
+ def self.title_data_file(title)
200
+ title_dir(title) + "#{title}.json"
201
+ end
202
+
203
+ # @pararm title [String] the title we care about
204
+ #
205
+ # @return [Pathname] The the local file containing the code of the version script
206
+ #####################
207
+ def self.version_script_file(title)
208
+ title_dir(title) + VERSION_SCRIPT_FILENAME
209
+ end
210
+
211
+ # @pararm title [String] the title we care about
212
+ #
213
+ # @return [Pathname] The the local file containing the code of the version script
214
+ #####################
215
+ def self.uninstall_script_file(title)
216
+ title_dir(title) + UNINSTALL_SCRIPT_FILENAME
217
+ end
218
+
219
+ # @pararm title [String] the title we care about
220
+ #
221
+ # @return [Pathname] The the local file containing the self-service icon
222
+ #####################
223
+ def self.ssvc_icon_file(title)
224
+ title_dir(title).children.select { |c| c.basename.to_s.start_with? SELF_SERVICE_ICON_FILENAME }.first
225
+ end
226
+
227
+ # Instantiate from the local JSON file containing the current values
228
+ # for the given title
229
+ #
230
+ # NOTE: All instantiation should happen using the #instantiate_title method
231
+ # in the server app instance. Please don't call this method directly
232
+ #
233
+ # @pararm title [String] the title we care about
234
+ # @return [Xolo::Server::Title] load an existing title
235
+ # from the on-disk JSON file
236
+ ######################
237
+ def self.load(title)
238
+ Xolo::Server.logger.debug "Loading title '#{title}' from file"
239
+ new parse_json(title_data_file(title).read)
240
+ end
241
+
242
+ # @param title [String] the title we are looking for
243
+ # @pararm cnx [Windoo::Connection] The Title Editor connection to use
244
+ # @return [Boolean] Does the given title exist in the Title Editor?
245
+ ###############################
246
+ def self.in_ted?(title, cnx:)
247
+ Windoo::SoftwareTitle.all_ids(cnx: cnx).include? title
248
+ end
249
+
250
+ # Is a title locked for updates?
251
+ #############################
252
+ def self.locked?(title)
253
+ curr_lock = Xolo::Server.object_locks.dig title, :expires
254
+ curr_lock && curr_lock > Time.now
255
+ end
256
+
257
+ # Attributes
258
+ ######################
259
+ ######################
260
+
261
+ # For each title there will be a smart group containing all macs
262
+ # that have any version of the title installed. The smart group
263
+ # will be named 'xolo-<title>-installed'
264
+ #
265
+ # It will be used as an exclusion for the initial auto-installation
266
+ # policy for each version since if the title is installed at all,
267
+ # any installation is not 'initial' but an update, and will be
268
+ # handled by the Patch Policy.
269
+ #
270
+ # Since there is one such group per title, it's name is stored here
271
+ #
272
+ # @return [String] the name of the smart group
273
+ attr_reader :jamf_installed_group_name
274
+
275
+ # For each title there will be a static group containing macs
276
+ # that should not get any automatic installs or updates, They
277
+ # should be 'frozen' at whatever version was installed when they
278
+ # were added to the group. It will be named 'xolo-<title>-frozen'
279
+ #
280
+ # It will be used as an exclusion for the installation
281
+ # policies and the patch policy for each version.
282
+ #
283
+ # Membership is maintained using `xadm freeze <title> <computer> [<computer> ...]`
284
+ # and `xadm thaw <title> <computer> [<computer> ...]`
285
+ #
286
+ # Use `xadm report <title> frozen` to see a list.
287
+ #
288
+ # If computer groups are used with freeze/thaw, they are expanded and their members
289
+ # added/removed individually in the static group
290
+ #
291
+ # Since there is one such group per title, it's name is stored here
292
+ #
293
+ # @return [String] the name of the smart group
294
+ attr_reader :jamf_frozen_group_name
295
+
296
+ # The name of the policy that does initial manual or self-service
297
+ # installs of the currently-released version of this title.
298
+ # It will be named 'xolo-<title>-install'
299
+ attr_reader :jamf_manual_install_released_policy_name
300
+
301
+ # If a title is uninstallable, it will have a script in Jamf
302
+ # named 'xolo-<title>-uninstall'
303
+ #
304
+ # @return [String] the name of the script to uninstall the title
305
+ attr_reader :jamf_uninstall_script_name
306
+
307
+ # If a title is uninstallable, it will have a policy in Jamf
308
+ # named 'xolo-<title>-uninstall' that will run the script of
309
+ # the same name, using a trigger of the same name.
310
+ #
311
+ # @return [String] the name of the policy to uninstall the title
312
+ attr_reader :jamf_uninstall_policy_name
313
+
314
+ # If a title is expirable, it will have a policy in Jamf
315
+ # named 'xolo-<title>-expire' that will run the expiration
316
+ # process daily, at checkin or using a trigger of the same name.
317
+ #
318
+ # @return [String] the name of the policy to uninstall the title
319
+ attr_reader :jamf_expire_policy_name
320
+
321
+ # The instance of Xolo::Server::App that instantiated this
322
+ # title object. This is how we access things that are available in routes
323
+ # and helpers, like the single Jamf and TEd
324
+ # connections for this App instance.
325
+ # @return [Xolo::Server::App] our Sinatra server app
326
+ attr_accessor :server_app_instance
327
+
328
+ # @return [Integer] The Windoo::SoftwareTitle#softwareTitleId
329
+ attr_accessor :ted_id_number
330
+
331
+ # when applying updates, the new data from xadm is stored
332
+ # here so it can be accessed by update-methods
333
+ # and compared to the current instance values
334
+ # both for updating the title, and the versions
335
+ #
336
+ # @return [Hash] The new data to apply as an update
337
+ attr_reader :new_data_for_update
338
+
339
+ # Also when applying updates, this will hold the
340
+ # changes being made: the differences between
341
+ # tne current attributes and the new_data_for_update
342
+ # We'll figure this out at the start of the update
343
+ # and can use it later to
344
+ # 1) avoid doing things we don't need to
345
+ # 2) log the changes in the change log at the very end
346
+ #
347
+ # This is a Hash with keys of the attribute names that have changed
348
+ # the values are Hashes with keys of :old and :new
349
+ # e.g. { release_groups: { old: ['foo'], new: ['bar'] } }
350
+ #
351
+ # @return [Hash] The changes being made
352
+ attr_reader :changes_for_update
353
+
354
+ # @return [Integer] The Jamf Pro ID for the self-service icon
355
+ # once it has been uploaded
356
+ attr_accessor :ssvc_icon_id
357
+
358
+ # @return [Symbol] The current action being taken on this title
359
+ # one of :creating, :updating, :deleting
360
+ attr_accessor :current_action
361
+
362
+ # @return [String] If current action is :releasing, this is the
363
+ # version being released
364
+ attr_accessor :releasing_version
365
+
366
+ # version_order is defined in ATTRIBUTES
367
+ alias versions version_order
368
+
369
+ # Constructor
370
+ ######################
371
+ ######################
372
+
373
+ # NOTE: be sure to only instantiate these using the
374
+ # servers 'instantiate_title' method, or else
375
+ # they might not have all the correct innards
376
+ def initialize(data_hash)
377
+ super
378
+
379
+ @ted_id_number ||= data_hash[:ted_id_number]
380
+ @jamf_patch_title_id ||= data_hash[:jamf_patch_title_id]
381
+ @version_order ||= []
382
+ @new_data_for_update = {}
383
+ @changes_for_update = {}
384
+ @jamf_installed_group_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_INSTALLED_GROUP_NAME_SUFFIX}"
385
+ @jamf_frozen_group_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_FROZEN_GROUP_NAME_SUFFIX}"
386
+
387
+ @jamf_manual_install_released_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}-install"
388
+
389
+ @jamf_uninstall_script_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_UNINSTALL_SUFFIX}"
390
+ @jamf_uninstall_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_UNINSTALL_SUFFIX}"
391
+ @jamf_expire_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_EXPIRE_SUFFIX}"
392
+ end
393
+
394
+ # Instance Methods
395
+ ######################
396
+ ######################
397
+
398
+ # @return [Hash]
399
+ ###################
400
+ def session
401
+ server_app_instance&.session || {}
402
+ # @session ||= {}
403
+ end
404
+
405
+ # @return [String]
406
+ ###################
407
+ def admin
408
+ session[:admin]
409
+ end
410
+
411
+ # @return [Boolean] Are we creating this title?
412
+ ###################
413
+ def creating?
414
+ current_action == :creating
415
+ end
416
+
417
+ # @return [Boolean] Are we updating this title?
418
+ ###################
419
+ def updating?
420
+ current_action == :updating
421
+ end
422
+
423
+ # @return [Boolean] Are we repairing this title?
424
+ ###################
425
+ def repairing?
426
+ current_action == :repairing
427
+ end
428
+
429
+ # @return [Boolean] Are we deleting this title?
430
+ ###################
431
+ def deleting?
432
+ current_action == :deleting
433
+ end
434
+
435
+ # @return [Boolean] Are we releasing a version this title?
436
+ ###################
437
+ def releasing?
438
+ current_action == :releasing
439
+ end
440
+
441
+ # Append a message to the progress stream file,
442
+ # optionally sending it also to the log
443
+ #
444
+ # @param message [String] the message to append
445
+ # @param log [Symbol] the level at which to log the message
446
+ # one of :debug, :info, :warn, :error, :fatal, or :unknown.
447
+ # Default is nil, which doesn't log the message at all.
448
+ #
449
+ # @return [void]
450
+ ###################
451
+ def progress(msg, log: :debug)
452
+ server_app_instance.progress msg, log: log
453
+ end
454
+
455
+ # @return [Windoo::Connection] a single Title Editor connection to use for
456
+ # the life of this instance
457
+ #############################
458
+ def ted_cnx
459
+ server_app_instance.ted_cnx
460
+ end
461
+
462
+ # @return [Jamf::Connection] a single Jamf Pro API connection to use for
463
+ # the life of this instance
464
+ #############################
465
+ def jamf_cnx(refresh: false)
466
+ server_app_instance.jamf_cnx refresh: refresh
467
+ end
468
+
469
+ # The title dir for this title on the server
470
+ # @return [Pathname]
471
+ #########################
472
+ def title_dir
473
+ @title_dir ||= self.class.title_dir title
474
+ end
475
+
476
+ # The title data file for this title on the server
477
+ # @return [Pathname]
478
+ #########################
479
+ def title_data_file
480
+ @title_data_file ||= self.class.title_data_file title
481
+ end
482
+
483
+ # @return [Pathname] The the local file containing the self-service icon
484
+ #####################
485
+ def ssvc_icon_file
486
+ @ssvc_icon_file ||= self.class.ssvc_icon_file title
487
+ end
488
+
489
+ # @return [Pathname] The the local file containing the code of the version script
490
+ #####################
491
+ def version_script_file
492
+ @version_script_file ||= self.class.version_script_file title
493
+ end
494
+
495
+ # The code of the version script, if any,
496
+ # considering the new data of any changes being made
497
+ #
498
+ # Returns nil if there is no version script, or if we are in the
499
+ # process of deleting it.
500
+ #
501
+ # @return [String] The string contents of the version_script, if any
502
+ ####################
503
+ def version_script_contents
504
+ return @version_script_contents if defined? @version_script_contents
505
+
506
+ curr_script =
507
+ if changes_for_update&.key? :version_script
508
+ # new, incoming script
509
+ changes_for_update[:version_script][:new]
510
+ else
511
+ # the current attribute value, might be Xolo::ITEM_UPLOADED
512
+ version_script
513
+ end
514
+
515
+ @version_script_contents =
516
+ if curr_script.pix_empty?
517
+ # no script, or deleting script
518
+ nil
519
+ elsif curr_script == Xolo::ITEM_UPLOADED
520
+ # use the one we have saved on disk
521
+ version_script_file.read
522
+ else
523
+ # this will be a new one from the changes_for_update
524
+ curr_script
525
+ end
526
+ end
527
+
528
+ # @return [Pathname] The the local file containing the code of the uninstall script
529
+ #####################
530
+ def uninstall_script_file
531
+ @uninstall_script_file ||= self.class.uninstall_script_file title
532
+ end
533
+
534
+ # The code of the uninstall_script , if any,
535
+ # considering the new data of any changes being made
536
+ #
537
+ # Returns nil if there is no uninstall_script, or if we are in the
538
+ # process of deleting it.
539
+ #
540
+ # @return [String] The string contents of the uninstall_script, if any
541
+ ####################
542
+ def uninstall_script_contents
543
+ return @uninstall_script_contents if defined? @uninstall_script_contents
544
+
545
+ # use any new/incoming value if we have any
546
+ # this might still be nil or an empty array if we are removing uninstallability
547
+ curr_script = changes_for_update.dig(:uninstall_script, :new) || changes_for_update.dig(:uninstall_ids, :new)
548
+ curr_script = nil if curr_script.pix_empty?
549
+
550
+ # otherwise use the existing value
551
+ curr_script ||= uninstall_script || uninstall_ids
552
+
553
+ # now get the actual script
554
+ @uninstall_script_contents =
555
+ if curr_script.pix_empty?
556
+ # removing uninstallability, or it was never added
557
+ nil
558
+ elsif curr_script == Xolo::ITEM_UPLOADED
559
+ # nothing changed, use the one we have saved on disk
560
+ uninstall_script_file.read
561
+ else
562
+ # this will be a new one from the changes_for_update
563
+ generate_uninstall_script curr_script
564
+ end
565
+
566
+ # log_debug "Uninstall script contents: #{@uninstall_script_contents}"
567
+ @uninstall_script_contents
568
+ end
569
+
570
+ # @param script_or_pkg_ids [String] The new uninstall script, or comma-separated list of pkg IDs
571
+ # @return [String, Array ] The uninstall script, provided or generated from the given pkg ids
572
+ #####################
573
+ def generate_uninstall_script(script_or_pkg_ids)
574
+ # Its already a script, validated by xadm to start with #!
575
+ return script_or_pkg_ids if script_or_pkg_ids.is_a? String
576
+
577
+ uninstall_script_template.sub 'PKG_IDS_FROM_XOLO_GO_HERE', script_or_pkg_ids.join(' ')
578
+ end
579
+
580
+ # @return [String] The template zsh script for uninstalling via pkgutil
581
+ #####################
582
+ def uninstall_script_template
583
+ # parent 1 = server
584
+ # parent 2 = xolo
585
+ # parent 3 = lib
586
+ # parent 4 = xolo gem
587
+ data_dir = Pathname.new(__FILE__).parent.parent.parent.parent + 'data'
588
+ template_file = data_dir + 'uninstall-pkgs-by-id.zsh'
589
+ template_file.read
590
+ end
591
+
592
+ # @return [String] The display name of a version script as a normal
593
+ # EA in Jamf, which can be used in Smart Groups and Adv Searches.
594
+ #####################
595
+ def jamf_normal_ea_name
596
+ @jamf_normal_ea_name ||= self.class.jamf_normal_ea_name title
597
+ end
598
+
599
+ # prepend a new version to the version_order
600
+ #
601
+ # @param version [String] the version to prepend
602
+ #
603
+ # @return [void]
604
+ ########################
605
+ def prepend_version(version)
606
+ lock
607
+ version_order.unshift version
608
+ save_local_data
609
+ ensure
610
+ unlock
611
+ end
612
+
613
+ # remove a version from the version_order
614
+ #
615
+ # @param version [String] the version to remove
616
+ #
617
+ # @return [void]
618
+ ########################
619
+ def remove_version(version)
620
+ lock
621
+ version_order.delete version
622
+ save_local_data
623
+ ensure
624
+ unlock
625
+ end
626
+
627
+ # instantiate a version if this title
628
+ #
629
+ # @return [Xolo::Server::Version]
630
+ ########################
631
+ def version_object(version)
632
+ log_debug "Instantiating version #{version} from Title instance #{title}"
633
+ server_app_instance.instantiate_version(title: self, version: version)
634
+ end
635
+
636
+ # @return [Array<Xolo::Server::Version>] An array of all current version objects
637
+ # NOTE: This might not be wise if hundreds of versions, but automated cleanup should
638
+ # help with that.
639
+ ########################
640
+ def version_objects(refresh: false)
641
+ @version_objects = nil if refresh
642
+ return @version_objects if @version_objects
643
+
644
+ @version_objects = version_order.map do |v|
645
+ version_object v
646
+ rescue Xolo::Core::Exceptions::NoSuchItemError
647
+ next if deleting?
648
+
649
+ raise
650
+ end
651
+ end
652
+
653
+ # @return [String] The URL path for the patch report for this title
654
+ #############################
655
+ def patch_report_rsrc
656
+ @patch_report_rsrc ||= "#{JPAPI_PATCH_TITLE_RSRC}/#{jamf_patch_title_id}/#{JPAPI_PATCH_REPORT_RSRC}"
657
+ end
658
+
659
+ # Save a new title, adding to the
660
+ # local filesystem, Jamf Pro, and the Title Editor as needed
661
+ #
662
+ # @return [void]
663
+ #########################
664
+ def create
665
+ lock
666
+
667
+ @current_action = :creating
668
+
669
+ self.creation_date = Time.now
670
+ self.created_by = admin
671
+ log_debug "creation_date: #{creation_date}, created_by: #{created_by}"
672
+
673
+ # this will create the title as needed in the Title Editor
674
+ create_title_in_ted
675
+ create_title_in_jamf
676
+
677
+ # save to file last, because saving to TitleEd and Jamf will
678
+ # add some data
679
+ progress 'Saving title data to Xolo server'
680
+ save_local_data
681
+
682
+ log_change msg: 'Title Created'
683
+
684
+ # ssvc icon is uploaded in a separate process, and the
685
+ # title data file will be updated as needed then.
686
+ ensure
687
+ unlock
688
+ end
689
+
690
+ # Update this title, updating to the
691
+ # local filesystem, Jamf Pro, and the Title Editor,
692
+ # and applying any changes to existing versions as needed.
693
+ #
694
+ # @param new_data [Hash] The new data sent from xadm
695
+ # @return [void]
696
+ #########################
697
+ def update(new_data)
698
+ lock
699
+
700
+ @current_action = :updating
701
+ @new_data_for_update = new_data
702
+ @changes_for_update = note_changes_for_update_and_log
703
+
704
+ if @changes_for_update.pix_empty?
705
+ progress 'No changes to make', log: :info
706
+ return
707
+ end
708
+
709
+ log_info "Updating title '#{title}' for admin '#{admin}'"
710
+ log_debug "Updating title with these changes: #{changes_for_update}"
711
+
712
+ # changelog - log the changes now, and
713
+ # if there is an error, we'll log that too
714
+ # saying the above changes were not completed and to
715
+ # look at the server log for details.
716
+ log_update_changes
717
+
718
+ # Do ted before doing things in Jamf
719
+ update_title_in_ted
720
+ update_title_in_jamf
721
+ update_local_instance_values
722
+ save_local_data
723
+
724
+ # if we already have a version script, and it hasn't changed, the new data should
725
+ # contain Xolo::ITEM_UPLOADED. If its nil, we shouldn't
726
+ # have one at all and should remove the old one if its there
727
+ delete_version_script_file unless new_data_for_update[:version_script]
728
+
729
+ # Do This at the end - after all the versions/patches have been updated.
730
+ # Jamf won't see the need for re-acceptance until after the title
731
+ # (and at least one patch) have been re-enabled.
732
+ accept_jamf_patch_ea if need_to_accept_jamf_patch_ea?
733
+
734
+ # any new self svc icon will be uploaded in a separate process
735
+ # and the local data will be updated again then
736
+ #
737
+ rescue => e
738
+ log_change msg: "ERROR: The update failed and the changes didn't all go through!\n#{e.class}: #{e.message}\nSee server log for details."
739
+
740
+ # re-raise for proper error handling in the server app
741
+ raise
742
+ ensure
743
+ unlock
744
+ end # update
745
+
746
+ # Update our instance attributes with any new data before
747
+ # saving the changes back out to the file system
748
+ # @return [void]
749
+ ###########################
750
+ def update_local_instance_values
751
+ # update instance data with new data before writing out to the filesystem.
752
+ # Do this last so that the instance values can be compared to
753
+ # new_data_for_update in the steps above.
754
+ # Also, those steps might have updated some server-specific attributes
755
+ # which will be saved to the file system as well.
756
+ ATTRIBUTES.each do |attr, deets|
757
+ # make sure these are updated elsewhere if needed,
758
+ # e.g. modification data.
759
+ next if deets[:read_only]
760
+ next unless deets[:cli]
761
+
762
+ new_val = new_data_for_update[attr]
763
+ old_val = send(attr)
764
+ next if new_val == old_val
765
+
766
+ log_debug "Updating Xolo Title attribute '#{attr}': '#{old_val}' -> '#{new_val}'"
767
+ send "#{attr}=", new_val
768
+ end
769
+
770
+ # update any other server-specific attributes here...
771
+ end
772
+
773
+ # Save our current data out to our JSON data file
774
+ # This overwrites the existing data.
775
+ #
776
+ # @return [void]
777
+ ##########################
778
+ def save_local_data
779
+ # create the dirs for the title
780
+ title_dir.mkpath
781
+ vdir = title_dir + Xolo::Server::Version::VERSIONS_DIRNAME
782
+ vdir.mkpath
783
+
784
+ save_version_script
785
+ save_uninstall_script
786
+
787
+ self.modification_date = Time.now
788
+ self.modified_by = admin
789
+ log_debug "Title '#{title}' noting modification by #{modified_by}"
790
+
791
+ # do we have a stored self service icon?
792
+ self.self_service_icon = ssvc_icon_file ? Xolo::ITEM_UPLOADED : nil
793
+
794
+ log_debug "Saving local title data to: #{title_data_file}"
795
+ title_data_file.pix_atomic_write to_json
796
+ end
797
+
798
+ # Save our current version script out to our local file,
799
+ # but only if we aren't using app_name and app_bundle_id
800
+ # and only if it's changed
801
+ #
802
+ # This won't delete the script if it's being removed, that
803
+ # happens elsewhere.
804
+ #
805
+ # This overwrites the existing data.
806
+ #
807
+ # @return [void]
808
+ ##########################
809
+ def save_version_script
810
+ return if app_name || app_bundle_id
811
+ return if version_script_contents.nil?
812
+
813
+ log_debug "Saving version_script to: #{version_script_file}"
814
+ version_script_file.pix_atomic_write version_script_contents
815
+
816
+ # the json file only stores 'uploaded' in the version_script attr.
817
+ self.version_script = Xolo::ITEM_UPLOADED
818
+ end
819
+
820
+ # Save our current uninstall script out to our local file.
821
+ #
822
+ # This won't delete the script if it's being removed, that
823
+ # happens elsewhere.
824
+ #
825
+ # This overwrites the existing data.
826
+ #
827
+ # @return [void]
828
+ ##########################
829
+ def save_uninstall_script
830
+ return if uninstall_script == Xolo::ITEM_UPLOADED || uninstall_ids == Xolo::ITEM_UPLOADED
831
+ return if uninstall_script_contents.nil?
832
+
833
+ log_debug "Saving uninstall script to: #{uninstall_script_file}"
834
+ uninstall_script_file.pix_atomic_write uninstall_script_contents
835
+
836
+ # the json file only stores 'uploaded' in uninstall_script
837
+ # The actual script is saved in its own file.
838
+ self.uninstall_script &&= Xolo::ITEM_UPLOADED
839
+ end
840
+
841
+ # are we uninstallable?
842
+ #
843
+ # @return [Boolean]
844
+ ##########################
845
+ def uninstallable?
846
+ uninstall_script || !uninstall_ids.pix_empty?
847
+ end
848
+
849
+ # Save the self_service_icon from the upload tmpfile
850
+ # to the file in the data dir.
851
+ #
852
+ # This is run by the upload route, not the
853
+ # create or update methods here.
854
+ # xadm does the upload after creating or updating the title
855
+ #
856
+ # @param tempfile [Pathname] The path to the uploaded tmp file
857
+ #
858
+ # @return [void]
859
+ ##########################
860
+ def save_ssvc_icon(tempfile, orig_filename)
861
+ lock
862
+ # here's where we'll store it on the server
863
+ ext_for_file = orig_filename.split(Xolo::DOT).last
864
+ new_basename = "#{SELF_SERVICE_ICON_FILENAME}.#{ext_for_file}"
865
+ new_icon_file = title_dir + new_basename
866
+
867
+ # delete any previous icon files
868
+ existing_icon_file = ssvc_icon_file
869
+ if existing_icon_file&.file?
870
+ log_debug "Deleting older icon file: #{existing_icon_file.basename}"
871
+ existing_icon_file.delete
872
+ end
873
+
874
+ log_debug "Saving self_service_icon '#{orig_filename}' to: #{new_basename}"
875
+ tempfile.rename new_icon_file
876
+
877
+ # the json file only stores 'uploaded' in the self_service_icon
878
+ # attr.
879
+ self.self_service_icon = Xolo::ITEM_UPLOADED
880
+ save_local_data
881
+ ensure
882
+ unlock
883
+ end
884
+
885
+ # Delete the version script file
886
+ #
887
+ # @return [void]
888
+ ##########################
889
+ def delete_version_script_file
890
+ return unless version_script_file.file?
891
+
892
+ log_debug "Deleting version script file: #{version_script_file}"
893
+
894
+ version_script_file.delete
895
+ end
896
+
897
+ # Delete the title and all of its version
898
+ # @return [void]
899
+ ##########################
900
+ def delete
901
+ lock
902
+ @current_action = :deleting
903
+
904
+ progress "Deleting all versions of #{title}...", log: :debug
905
+ # Delete them in reverse order (oldest first) so the jamf server doesn't
906
+ # see each older version as being 'released' again as newer
907
+ # ones are deleted.
908
+ version_objects.reverse.each do |vers|
909
+ # vers might be nil if it was already deleted
910
+ # e.g. a prev. attempt to delete the title failed partway through
911
+ vers&.delete update_title: false
912
+ end
913
+
914
+ delete_title_from_ted
915
+
916
+ delete_title_from_jamf
917
+
918
+ delete_changelog
919
+
920
+ progress "Deleting Xolo server data for title '#{title}'", log: :info
921
+ title_dir.rmtree
922
+ ensure
923
+ unlock
924
+ end
925
+
926
+ # Release a version of this title
927
+ #
928
+ # @param version_to_release [String] the version to release
929
+ #
930
+ # @return [void]
931
+ ##########################
932
+ def release(version_to_release)
933
+ lock
934
+ @current_action = :releasing
935
+ @releasing_version = version_to_release
936
+
937
+ validate_release(version_to_release)
938
+
939
+ progress "Releasing version #{version_to_release} of title '#{title}'", log: :info
940
+
941
+ update_versions_for_release version_to_release
942
+
943
+ # update the title
944
+ self.released_version = version_to_release
945
+ save_local_data
946
+ ensure
947
+ unlock
948
+ end
949
+
950
+ # are we OK releasing a given version?
951
+ # @return [void]
952
+ ######################################
953
+ def validate_release(version_to_release)
954
+ if released_version == version_to_release
955
+ raise Xolo::InvalidDataError,
956
+ "Version '#{version_to_release}' of title '#{title}' is already released"
957
+ end
958
+
959
+ return if versions.include? version_to_release
960
+
961
+ raise Xolo::NoSuchItemError,
962
+ "No version '#{version_to_release}' for title '#{title}'"
963
+ end
964
+
965
+ # Update all versions when releasing one
966
+ # @param version_to_release [String] the version to release
967
+ # @return [void]
968
+ ##############################
969
+ def update_versions_for_release(version_to_release)
970
+ # get the Version objects and figure out our starting point, but process
971
+ # them in reverse order so that we don't have two released versions at once
972
+ all_versions = version_objects.reverse
973
+ vobj_to_release = all_versions.find { |v| v.version == version_to_release }
974
+ vobj_current_release = all_versions.find { |v| v.version == released_version }
975
+
976
+ rollback = vobj_current_release && vobj_to_release < vobj_current_release
977
+
978
+ progress "Rolling back from version #{released_version}", log: :info if rollback
979
+
980
+ all_versions.each do |vobj|
981
+ # This is the one we are releasing
982
+ if vobj == vobj_to_release
983
+ release_version(vobj, rollback: rollback)
984
+
985
+ # This one is older than the one we're releasing
986
+ # so its either deprecated or skipped
987
+ elsif vobj < vobj_to_release
988
+ deprecate_or_skip_version(vobj)
989
+
990
+ # this one is newer than the one we're releasing
991
+ # revert to pilot if appropriate
992
+ else
993
+ reset_version_to_pilot(vobj)
994
+
995
+ end # if vobj == vobj_to_release
996
+ end # all_versions.each
997
+ end
998
+
999
+ # release a specific version
1000
+ # @param vobj [Xolo::Server::Version] the version object to be released
1001
+ # @return [void]
1002
+ #######################################
1003
+ def release_version(vobj, rollback:)
1004
+ vobj.release rollback: rollback
1005
+
1006
+ # update the jamf_manual_install_released_policy to install this version
1007
+ msg = "Jamf: Setting policy #{jamf_manual_install_released_policy_name} to install the package for version '#{vobj.version}'"
1008
+ progress msg, log: :info
1009
+
1010
+ pol = jamf_manual_install_released_policy
1011
+ pol.package_ids.each { |pid| pol.remove_package pid }
1012
+ pol.add_package vobj.jamf_pkg_id
1013
+ pol.save
1014
+ end
1015
+
1016
+ # Deprecate or skip a version
1017
+ # @param vobj [Xolo::Server::Version] the version object to be deprecated or skipped
1018
+ # @return [void]
1019
+ #######################################
1020
+ def deprecate_or_skip_version(vobj)
1021
+ # don't do anything if the status is already deprecated or skipped
1022
+
1023
+ # but if its released, we need to deprecate it
1024
+ vobj.deprecate if vobj.status == Xolo::Server::Version::STATUS_RELEASED
1025
+
1026
+ # and skip it if its in pilot
1027
+ vobj.skip if vobj.status == Xolo::Server::Version::STATUS_PILOT
1028
+ end
1029
+
1030
+ # reset a version to pilot status, this happens when rolling back
1031
+ # (releasing a version older than the current release)
1032
+ # @param vobj [Xolo::Server::Version] the version object to be deprecated or skipped
1033
+ # @return [void]
1034
+ #############################
1035
+ def reset_version_to_pilot(vobj)
1036
+ # do nothing if its in pilot
1037
+ return if vobj.status == Xolo::Server::Version::STATUS_PILOT
1038
+
1039
+ # this should be redundant with the above?
1040
+ return unless rollback
1041
+
1042
+ # if we're here, we're rolling back to something older than this
1043
+ # version, and this version is currently released, deprecated or skipped.
1044
+ # We need to reset it to pilot.
1045
+ vobj.reset_to_pilot
1046
+ end
1047
+
1048
+ # Repair this title, and optionally all of its versions.
1049
+ #
1050
+ # Look at the Title Editor title object, and ensure it's correct based on the local data file.
1051
+ # - display name
1052
+ # - publisher
1053
+ # - EA or app-data
1054
+ # - ea name 'xolo-<title>'
1055
+ # - requirement criteria
1056
+ # - stub version if needed
1057
+ # - enabled
1058
+ #
1059
+ # Then look at the various Jamf objects pertaining to this title, and ensure they are correct
1060
+ # - Accept Patch EA
1061
+ # - Normal EA 'xolo-<title>-installed-version'
1062
+ # - title-installed smart group 'xolo-<title>-installed'
1063
+ # - frozen static group 'xolo-<title>-frozen'
1064
+ # - manual/SSvc install-current-release policy 'xolo-<title>-install'
1065
+ # - trigger 'xolo-<title>-install'
1066
+ # - ssvc icon
1067
+ # - ssvc category
1068
+ # - description
1069
+ # - if uninstallable
1070
+ # - uninstall script 'xolo-<title>-uninstall'
1071
+ # - uninstall policy 'xolo-<title>-uninstall'
1072
+ # - if expirable
1073
+ # - expire policy 'xolo-<title>-expire'
1074
+ # - trigger 'xolo-<title>-expire'
1075
+ #
1076
+ # @param repair_versions [Boolean] run the repair method on all versions?
1077
+ # @return [void]
1078
+ ##################################
1079
+ def repair(repair_versions: false)
1080
+ lock
1081
+ @current_action = :repairing
1082
+ chg_log_msg = repair_versions ? 'Repairing title and all versions' : 'Repairing title only'
1083
+ log_change msg: chg_log_msg
1084
+
1085
+ progress "Starting repair of title '#{title}'"
1086
+ repair_ted_title
1087
+ repair_jamf_title_objects
1088
+ return unless repair_versions
1089
+
1090
+ version_objects.each do |vobj|
1091
+ progress '#########'
1092
+ vobj.repair
1093
+ end
1094
+ ensure
1095
+ unlock
1096
+ end
1097
+
1098
+ # Is this title locked for updates?
1099
+ #############################
1100
+ def locked?
1101
+ self.class.locked?(title)
1102
+ end
1103
+
1104
+ # Lock this title for updates
1105
+ #############################
1106
+ def lock
1107
+ raise Xolo::ServerError, 'Server is shutting down' if Xolo::Server.shutting_down?
1108
+
1109
+ while locked?
1110
+ log_debug "Waiting for update lock on title '#{title}'..." if (Time.now.to_i % 5).zero?
1111
+ sleep 0.33
1112
+ end
1113
+ Xolo::Server.object_locks[title] ||= { versions: {} }
1114
+
1115
+ exp = Time.now + Xolo::Server::ObjectLocks::OBJECT_LOCK_LIMIT
1116
+ Xolo::Server.object_locks[title][:expires] = exp
1117
+ log_debug "Locked title '#{title}' for updates until #{exp}"
1118
+ end
1119
+
1120
+ # Unlock this v for updates
1121
+ #############################
1122
+ def unlock
1123
+ curr_lock = Xolo::Server.object_locks.dig title, :expires
1124
+ return unless curr_lock
1125
+
1126
+ Xolo::Server.object_locks[title].delete :expires
1127
+ log_debug "Unlocked title '#{title}' for updates"
1128
+ end
1129
+
1130
+ # Add more server-specific data to our hash
1131
+ ###########################
1132
+ def to_h
1133
+ hash = super
1134
+ hash[:ted_id_number] = ted_id_number
1135
+ hash[:ssvc_icon_id] = ssvc_icon_id
1136
+ hash
1137
+ end
1138
+
1139
+ end # class Title
1140
+
1141
+ end # module Admin
1142
+
1143
+ end # module Xolo