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,156 @@
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 Helpers
16
+
17
+ # constants and methods for accessing the Jamf Pro server
18
+ # from the Xolo server
19
+ #
20
+ # This is used both as a 'helper' in the Sinatra server,
21
+ # and an included mixin for the Xolo::Server::Title and
22
+ # Xolo::Server::Version classes.
23
+ #
24
+ # This means methods here are available in instances of
25
+ # those classes, and in all routes, views, and helpers in
26
+ # Sinatra.
27
+ #
28
+ module JamfPro
29
+
30
+ # Constants
31
+ #
32
+ ##############################
33
+ ##############################
34
+
35
+ PATCH_REPORT_UNKNOWN_VERSION = 'UNKNOWN_VERSION'
36
+ PATCH_REPORT_JPAPI_PAGE_SIZE = 500
37
+
38
+ # Module methods
39
+ #
40
+ # These are available as module methods but not as 'helper'
41
+ # methods in sinatra routes & views.
42
+ #
43
+ ##############################
44
+ ##############################
45
+
46
+ # when this module is included
47
+ ##############################
48
+ def self.included(includer)
49
+ Xolo.verbose_include includer, self
50
+ end
51
+
52
+ # when this module is extended
53
+ def self.extended(extender)
54
+ Xolo.verbose_extend extender, self
55
+ end
56
+
57
+ # Instance methods
58
+ #
59
+ # These are available directly in sinatra routes and views
60
+ #
61
+ ##############################
62
+ ##############################
63
+
64
+ # @return [String] The start of the Jamf Pro URL for GUI/WebApp access
65
+ ################
66
+ def jamf_gui_url
67
+ return @jamf_gui_url if @jamf_gui_url
68
+
69
+ host = Xolo::Server.config.jamf_gui_hostname
70
+ host ||= Xolo::Server.config.jamf_hostname
71
+ port = Xolo::Server.config.jamf_gui_port
72
+ port ||= Xolo::Server.config.jamf_port
73
+
74
+ @jamf_gui_url = "https://#{host}:#{port}"
75
+ end
76
+
77
+ # A connection to Jamf Pro via ruby-jss.
78
+ #
79
+ # We don't use the default connection but
80
+ # use this method to create standalone ones as needed
81
+ # and ensure they are disconnected, (or will timeout)
82
+ # when we are done.
83
+ #
84
+ # TODO: allow using APIClients
85
+ #
86
+ # @return [Jamf::Connection] A connection object
87
+ ##########################
88
+ def jamf_cnx(refresh: false)
89
+ if refresh
90
+ @jamf_cnx = nil
91
+ log_debug 'Jamf: Refreshing Jamf connection'
92
+ end
93
+
94
+ return @jamf_cnx if @jamf_cnx
95
+
96
+ @jamf_cnx = Jamf::Connection.new(
97
+ name: "jamf-pro-cnx-#{Time.now.strftime('%F-%T')}",
98
+ host: Xolo::Server.config.jamf_hostname,
99
+ port: Xolo::Server.config.jamf_port,
100
+ verify_cert: Xolo::Server.config.jamf_verify_cert,
101
+ ssl_version: Xolo::Server.config.jamf_ssl_version,
102
+ open_timeout: Xolo::Server.config.jamf_open_timeout,
103
+ timeout: Xolo::Server.config.jamf_timeout,
104
+ user: Xolo::Server.config.jamf_api_user,
105
+ pw: Xolo::Server.config.jamf_api_pw,
106
+ keep_alive: false
107
+ )
108
+ log_debug "Jamf: Connected to Jamf Pro at #{@jamf_cnx.base_url} as user '#{Xolo::Server.config.jamf_api_user}'. KeepAlive: #{@jamf_cnx.keep_alive?}, Expires: #{@jamf_cnx.token.expires}. cnx ID: #{@jamf_cnx.object_id}"
109
+
110
+ @jamf_cnx
111
+ end
112
+
113
+ # The id of the 'xolo' category in Jamf Pro.s
114
+ #
115
+ def jamf_xolo_category_id
116
+ @jamf_xolo_category_id ||=
117
+ if Jamf::Category.all_names(cnx: jamf_cnx).include? Xolo::Server::JAMF_XOLO_CATEGORY
118
+ Jamf::Category.valid_id(Xolo::Server::JAMF_XOLO_CATEGORY, cnx: jamf_cnx).to_s
119
+ else
120
+ Jamf::Category.create(name: Xolo::Server::JAMF_XOLO_CATEGORY, cnx: jamf_cnx).save
121
+ end
122
+ end
123
+
124
+ # if there's a forced_exclusion group defined in the server config
125
+ # return it's name, but only if it exists in jamf. If it doesn't
126
+ # return nil and alert someone
127
+ #
128
+ # @return [String] The valid name of the forced exclusion group
129
+ #####################
130
+ def valid_forced_exclusion_group_name
131
+ return @valid_forced_exclusion_group_name if defined?(@valid_forced_exclusion_group_name)
132
+
133
+ the_grp_name = Xolo::Server.config.forced_exclusion
134
+
135
+ if the_grp_name
136
+ if Jamf::ComputerGroup.all_names(cnx: jamf_cnx).include? the_grp_name
137
+ @valid_forced_exclusion_group_name = the_grp_name
138
+ else
139
+ msg = "ERROR: The forced_exclusion group '#{Xolo::Server.config.forced_exclusion}' in xolo server config does not exist in Jamf"
140
+ log_error msg, alert: true
141
+ @valid_forced_exclusion_group_name = nil
142
+ end
143
+
144
+ # not in config
145
+ else
146
+ @valid_forced_exclusion_group_name = nil
147
+ end
148
+ end
149
+
150
+ end # JamfPro
151
+
152
+ end # Helpers
153
+
154
+ end # Server
155
+
156
+ end # module Xolo
@@ -0,0 +1,97 @@
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 Helpers
16
+
17
+ # This is mixed in to Xolo::Server::App (as a helper, available in route processing)
18
+ # and in Xolo::Server::Title and Xolo::Server::Version,
19
+ # for simplified access to the main server logger, with access to session IDs
20
+ #
21
+ # The Title and Version objects must be instantiated with the current session object
22
+ # in order for this to work.
23
+ #
24
+ # See Xolo::Server::Helpers::Titles#instantiate_title for how this happens
25
+ #
26
+ # All those things need have have #session set before calling the log_* methods
27
+ module Log
28
+
29
+ # Module Methods
30
+ #######################
31
+ #######################
32
+
33
+ # when this module is included
34
+ def self.included(includer)
35
+ Xolo.verbose_include includer, self
36
+ end
37
+
38
+ # Instance Methods
39
+ #######################
40
+ ######################
41
+
42
+ ###############################
43
+ def logger
44
+ Xolo::Server.logger
45
+ end
46
+
47
+ ###############################
48
+ def session_svr_obj_id
49
+ return @session_svr_obj_id if @session_svr_obj_id
50
+
51
+ @session_svr_obj_id =
52
+ ("#{session[:xolo_id]}-#{object_id}" if session[:xolo_id])
53
+ end
54
+
55
+ ###############################
56
+ def log_debug(msg, alert: false)
57
+ logger.debug(session_svr_obj_id) { msg }
58
+ send_alert msg, :DEBUG if alert
59
+ end
60
+
61
+ ###############################
62
+ def log_info(msg, alert: false)
63
+ logger.info(session_svr_obj_id) { msg }
64
+ send_alert msg, :INFO if alert
65
+ end
66
+
67
+ ###############################
68
+ def log_warn(msg, alert: false)
69
+ logger.warn(session_svr_obj_id) { msg }
70
+ send_alert msg, :WARNING if alert
71
+ end
72
+
73
+ ###############################
74
+ def log_error(msg, alert: false)
75
+ logger.error(session_svr_obj_id) { msg }
76
+ send_alert msg, :ERROR if alert
77
+ end
78
+
79
+ ###############################
80
+ def log_fatal(msg, alert: false)
81
+ logger.fatal(session_svr_obj_id) { msg }
82
+ send_alert msg, :FATAL if alert
83
+ end
84
+
85
+ ###############################
86
+ def log_unknown(msg, alert: false)
87
+ logger.unknown(session_svr_obj_id) { msg }
88
+ send_alert msg, :UNKNOWN if alert
89
+ end
90
+
91
+ end # Log
92
+
93
+ end # Helpers
94
+
95
+ end # Server
96
+
97
+ end # module Xolo
@@ -0,0 +1,401 @@
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
+ # frozen_string_literal: true
8
+
9
+ module Xolo
10
+
11
+ module Server
12
+
13
+ module Helpers
14
+
15
+ # Nightly cleanup of deprecated and skipped packages.
16
+ #
17
+ # Also, alerts will be posted, and Emails will be sent to the
18
+ # admins who added versions that have been in pilot for more than
19
+ # some period of time.
20
+ #
21
+ #
22
+ module Maintenance
23
+
24
+ # when this module is included
25
+ def self.included(includer)
26
+ Xolo.verbose_include includer, self
27
+ end
28
+
29
+ # Constants
30
+ #####################################
31
+
32
+ # At what hour should the nightly cleanup run?
33
+ CLEANUP_HOUR = 2
34
+
35
+ # on which day of the month should we send the unreleased pilot notifications?
36
+ UNRELEASED_PILOTS_NOTIFICATION_DAY = 1
37
+
38
+ # Once a version becomes deprecated, it will
39
+ # be automatically deleted this many days later.
40
+ # If not set in the server config, this is
41
+ # the default value.
42
+ # use 0 or less to disable cleanup of deprecated versions
43
+ DFT_DEPRECATED_LIFETIME_DAYS = 30
44
+
45
+ # If a pilot has not been released in this many
46
+ # days, notify someone about it weekly, asking
47
+ # to release it or delete it.
48
+ # If not set in the server config, this is the
49
+ # default value.
50
+ DFT_UNRELEASED_PILOTS_NOTIFICATION_DAYS = 180
51
+
52
+ # when doing a full shutdown, we need to unload the launchd plist
53
+ SERVER_LAUNCHD_PLIST = Pathname.new '/Library/LaunchDaemons/com.pixar.xoloserver.plist'
54
+
55
+ # Module Methods
56
+ #####################################
57
+
58
+ # A mutex for the cleanup process
59
+ #
60
+ # TODO: use Concrrent Ruby instead of Mutex
61
+ #
62
+ # @return [Mutex] the mutex
63
+ #####################
64
+ def self.cleanup_mutex
65
+ @cleanup_mutex ||= Mutex.new
66
+ end
67
+
68
+ # nightly cleanup is done by a Concurrent::TimerTask, which checks every
69
+ # hour to see if it should do anything.
70
+ #
71
+ # It will only do the cleanup if the current time is in the 2am hour
72
+ # (02:00 - 02:59)
73
+ #
74
+ # We trigger the cleanup by POSTing to /cleanup, so that it runs
75
+ # in the context of a request, having access to Title and Version instantiation.
76
+ #
77
+ # @return [Concurrent::TimerTask] the timed task to do log rotation
78
+ ######################################
79
+ def self.cleanup_timer_task
80
+ return @cleanup_timer_task if @cleanup_timer_task
81
+
82
+ @cleanup_timer_task =
83
+ Concurrent::TimerTask.new(execution_interval: 3600) { post_to_start_cleanup }
84
+
85
+ Xolo::Server.logger.info 'Created Concurrent::TimerTask for nightly cleanup.'
86
+ @cleanup_timer_task
87
+ end
88
+
89
+ # When was our last cleanup?
90
+ # @return [Time] the time of the last cleanup, or the epoch if never
91
+ ######################################
92
+ def self.last_cleanup
93
+ @last_cleanup ||= Time.at(0)
94
+ end
95
+
96
+ # Set the time of the last cleanup
97
+ # @param time [Time] the time of the last cleanup
98
+ # @return [Time] the time of the last cleanup
99
+ ######################################
100
+ def self.last_cleanup=(time)
101
+ @last_cleanup = time
102
+ end
103
+
104
+ # post to the server to start the cleanup process
105
+ # This is done so that the cleanup can run in the context of a request,
106
+ # having access to Title and Version instantiation.
107
+ #
108
+ # @param force [Boolean] force the cleanup to run now
109
+ # @return [void]
110
+ ######################################
111
+ def self.post_to_start_cleanup(force: false)
112
+ if Xolo::Server.shutting_down?
113
+ Xolo::Server.logger.info 'Not starting cleanup, server is shutting down'
114
+ return
115
+ end
116
+
117
+ # only run the cleanup if it's between 2am and 3am
118
+ # and the last one was more than 23 hrs ago
119
+ last_cleanup_hrs_ago = (Time.now - last_cleanup) / 3600
120
+ return unless force || (Time.now.hour == CLEANUP_HOUR && last_cleanup_hrs_ago > 23)
121
+
122
+ uri = URI.parse "https://#{Xolo::Server::Helpers::Auth::IPV4_LOOPBACK}/maint/cleanup-internal"
123
+ https = Net::HTTP.new(uri.host, uri.port)
124
+ https.use_ssl = true
125
+ # The server cert may be self-signed and/or doesn't
126
+ # match the hostname, so we need to disable verification
127
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
128
+
129
+ request = Net::HTTP::Post.new(uri.path)
130
+ request['Authorization'] = Xolo::Server::Helpers::Auth.internal_auth_token_header
131
+
132
+ response = https.request(request)
133
+ Xolo::Server.logger.info "Cleanup request response: #{response.code} #{response.body}"
134
+ end
135
+
136
+ # Cleanup things that need to be cleaned up
137
+ # @return [void]
138
+ ################################
139
+ def run_cleanup
140
+ if Xolo::Server.shutting_down?
141
+ log_info 'Cleanup: Not starting cleanup, server is shutting down'
142
+ return
143
+ end
144
+ # TODO: Use Concurrent ruby rather than this instance variable
145
+ mutex = Xolo::Server::Helpers::Maintenance.cleanup_mutex
146
+
147
+ if mutex.locked?
148
+ log_warn 'Cleanup: already running, skipping this run'
149
+ return
150
+ end
151
+ mutex.lock
152
+ log_info 'Cleanup: starting'
153
+
154
+ # add new cleanup tasks/methods here
155
+ accept_title_editor_eas
156
+ cleanup_versions
157
+
158
+ log_info 'Cleanup: complete'
159
+ ensure
160
+ mutex&.unlock
161
+ end
162
+
163
+ # look for any titles that need their Title Editor EA's accepted,
164
+ # and auto accept them if we need to
165
+ # @return [void]
166
+ ######################################
167
+ def accept_title_editor_eas
168
+ unless Xolo::Server.config.jamf_auto_accept_xolo_eas
169
+ log_info 'Cleanup: The xolo server is not configured to auto-accept Title Editor EAs'
170
+ return
171
+ end
172
+
173
+ log_info 'Cleanup: Looking for Title Editor EAs to auto-accept'
174
+
175
+ # TODO: Be DRY with this stuff and similar in title_jamf_access.rb
176
+ Xolo::Server::Title.all_titles.each do |title|
177
+ title_obj = instantiate_title title
178
+ next unless title_obj.jamf_patch_ea_awaiting_acceptance?
179
+
180
+ log_info "Cleanup: Auto-accepting Title Editor EA for title '#{title}'"
181
+ title_obj.accept_jamf_patch_ea_via_api
182
+ rescue => e
183
+ log_error "Cleanup: Error auto-accepting Title Editor EA for title '#{title}': #{e}"
184
+ end # Xolo::Server::Title.all_titles.each
185
+
186
+ log_info 'Cleanup: Done with Title Editor EAs to auto-accept'
187
+ end
188
+
189
+ # Cleanup versions.
190
+ # @return [void]
191
+ ################################
192
+ def cleanup_versions
193
+ log_info 'Cleanup: cleaning up deprecated and skipped versions'
194
+
195
+ Xolo::Server::Title.all_titles.each do |title|
196
+ title_obj = instantiate_title title
197
+
198
+ title_obj.version_objects.each do |version|
199
+ if version.deprecated?
200
+ cleanup_deprecated_version version
201
+ elsif version.skipped?
202
+ cleanup_skipped_version version
203
+ end # case
204
+ end # each version
205
+
206
+ notify_admins_of_unreleased_pilots(title_obj)
207
+ end # each title
208
+
209
+ Xolo::Server::Helpers::Maintenance.last_cleanup = Time.now
210
+ log_info 'Cleanup: versions cleanup complete'
211
+ end
212
+
213
+ # Cleanup a deprecated version.
214
+ # @param version [Xolo::Server::Version] the version to cleanup
215
+ # @return [void]
216
+ ################################
217
+ def cleanup_deprecated_version(version)
218
+ # do nothing if the deprecated_lifetime_days is 0 or less
219
+ return unless deprecated_lifetime_days.positive?
220
+
221
+ # how many days has this version been deprecated?
222
+ days_deprecated = (Time.now - version.deprecation_date) / 86_400
223
+ return unless days_deprecated > deprecated_lifetime_days
224
+
225
+ log_info "Cleanup: Deleting deprecated version '#{version.version}' of title '#{version.title}'"
226
+ version.delete
227
+ end
228
+
229
+ # Cleanup a skipped version.
230
+ # @param version [Xolo::Server::Version] the version to cleanup
231
+ # @return [void]
232
+ ################################
233
+ def cleanup_skipped_version(version)
234
+ return if Xolo::Server.config.keep_skipped_versions
235
+
236
+ log_info "Cleanup: Deleting skipped version '#{version.version}' of title '#{version.title}'"
237
+ version.delete
238
+ end
239
+
240
+ # Notify the admins about unreleased pilots if needed
241
+ # @return [void]
242
+ ################################
243
+ def notify_admins_of_unreleased_pilots(title_obj)
244
+ return unless Time.now.day == UNRELEASED_PILOTS_NOTIFICATION_DAY
245
+ return unless unreleased_pilots_notification_days.positive?
246
+ return unless title_obj.latest_version
247
+
248
+ latest_vers_obj = instantiate_version title: title_obj, version: title_obj.latest_version
249
+ return unless latest_vers_obj.pilot?
250
+
251
+ days_in_pilot = ((Time.now - latest_vers_obj.creation_date) / 86_400).to_i
252
+
253
+ return unless days_in_pilot > unreleased_pilots_notification_days
254
+
255
+ alert_msg = "Cleanup: Notifying #{title_obj.contact_email} about unreleased pilot '#{latest_vers}' of title '#{title_obj.title}', in pilot for #{days_in_pilot} days"
256
+
257
+ log_info alert_msg
258
+ send_alert alert_msg
259
+
260
+ email_msg = <<~MSG
261
+ The newest version '#{latest_vers_obj.version}' of title '#{title_obj.title}' has been in pilot for #{days_in_pilot} days, which makes it seem like it's not going to be released.
262
+
263
+ To reduce clutter, please consider releasing it, deleting it, or deleting the whole title if it's no longer needed.
264
+
265
+ If this is intentional, you can ignore this monthly message.
266
+ MSG
267
+ send_email to: title_obj.contact_email, subject: 'Unreleased Pilot Notification', msg: email_msg
268
+ end
269
+
270
+ # how many days can a version be deprecated?
271
+ # @return [Integer] the number of days a version can be deprecated
272
+ ################################
273
+ def deprecated_lifetime_days
274
+ @deprecated_lifetime_days ||= Xolo::Server.config.deprecated_lifetime_days || DFT_DEPRECATED_LIFETIME_DAYS
275
+ end
276
+
277
+ # Notify the admins about unreleased pilots when the newest one is older than
278
+ # this many days.
279
+ def unreleased_pilots_notification_days
280
+ @unreleased_pilots_notification_days ||=
281
+ Xolo::Server.config.unreleased_pilots_notification_days || DFT_UNRELEASED_PILOTS_NOTIFICATION_DAYS
282
+ end
283
+
284
+ # Shutdown the server
285
+ # @return [void]
286
+ ################################
287
+ def shutdown_server(restart)
288
+ # let all the routes know we are shutting down
289
+ Xolo::Server.shutting_down = true
290
+
291
+ progress "Server Shutdown by #{session[:admin]}", log: :info
292
+
293
+ stop_cleanup_timer_task
294
+ stop_log_rotation_timer_task
295
+ shutdown_pkg_deletion_pool
296
+ wait_for_object_locks
297
+ wait_for_progress_streams
298
+
299
+ # without unloading the launchd job, the server will restart automatically
300
+ # when we tell it to quit
301
+ if restart
302
+ progress 'Restarting the server now', log: :info
303
+ Xolo::Server::App.quit!
304
+ else
305
+ progress 'Shutting down the server now', log: :info
306
+ unload_server_launchd
307
+ end
308
+ end
309
+
310
+ # full shutdown of the server by unloading the launchd plist
311
+ # @return [void]
312
+ ################################
313
+ def unload_server_launchd
314
+ log_info 'Unloading the server launchd plist'
315
+ system "/bin/launchctl unload #{SERVER_LAUNCHD_PLIST}"
316
+ end
317
+
318
+ # Stop the cleanup timer task
319
+ # @return [void]
320
+ ################################
321
+ def stop_cleanup_timer_task
322
+ progress 'Stopping the cleanup timer task', log: :info
323
+ Xolo::Server::Helpers::Maintenance.cleanup_timer_task.shutdown
324
+ end
325
+
326
+ # Stop the log rotation timer task
327
+ # @return [void]
328
+ ################################
329
+ def stop_log_rotation_timer_task
330
+ progress 'Stopping the log rotation timer task', log: :info
331
+ Xolo::Server::Log.log_rotation_timer_task.shutdown
332
+ end
333
+
334
+ # Wait for all object locks to be released
335
+ # @return [void]
336
+ ################################
337
+ def wait_for_object_locks
338
+ Xolo::Server.remove_expired_object_locks
339
+
340
+ until Xolo::Server.object_locks.empty?
341
+ progress 'Waiting for object locks to be released', log: :info
342
+ log_debug "Object locks: #{Xolo::Server.object_locks.inspect}"
343
+ sleep 5
344
+ Xolo::Server.remove_expired_object_locks
345
+ end
346
+ progress 'All object locks released', log: :info
347
+ end
348
+
349
+ # Wait for all progress streams to finish
350
+ # @return [void]
351
+ ################################
352
+ def wait_for_progress_streams
353
+ prefix = Xolo::Server::Helpers::ProgressStreaming::PROGRESS_THREAD_NAME_PREFIX
354
+ prog_threads = Thread.list.select { |th| th.name.to_s.start_with? prefix }
355
+ # remove our own thread from the list
356
+ prog_threads.delete Thread.current
357
+ prog_threads.delete @streaming_thread
358
+
359
+ until prog_threads.empty?
360
+ progress 'Waiting for progress streams to finish', log: :info
361
+ log_debug "Progress stream threads: #{prog_threads.map(&:name)}}"
362
+ sleep 5
363
+ prog_threads = Thread.list.select { |th| th.name.to_s.start_with? prefix }
364
+ # remove our own thread from the list
365
+ prog_threads.delete Thread.current
366
+ prog_threads.delete @streaming_thread
367
+ end
368
+ progress 'All progress streams finished', log: :info
369
+ end
370
+
371
+ # Shutdown the pkg deletion pool
372
+ # @return [void]
373
+ ################################
374
+ def shutdown_pkg_deletion_pool
375
+ # Start the shutdown of the pkg_deletion_pool. Will finish anything
376
+ # in the queue, but not accept any new tasks.
377
+ pkg_pool = Xolo::Server::Version.pkg_deletion_pool
378
+ pkg_pool.shutdown
379
+ pkg_pool_shutdown_start = Time.now
380
+ progress 'Shutting down pkg deletion pool', log: :info
381
+ # returns true when shutdown is complete
382
+ until pkg_pool.wait_for_termination(20)
383
+ msg = "..Waiting for pkg deletion pool to finish, processing: #{pkg_pool.length}, in queue: #{pkg_pool.queue_length}"
384
+ progress msg, log: :debug
385
+ next unless Time.now - pkg_pool_shutdown_start > Xolo::Server::Constants::MAX_JAMF_WAIT_FOR_PKG_DELETION
386
+
387
+ msg = 'ERROR: Timeout waiting for pkg deletion pool to finish, some pkgs may not be deleted'
388
+ progress msg, log: :error
389
+ pkg_pool.kill
390
+ break
391
+ end
392
+ progress 'Pkg deletion queue is empty'
393
+ end
394
+
395
+ end # module Maintenance
396
+
397
+ end # module Helpers
398
+
399
+ end # Server
400
+
401
+ end # module