xolo-server 2.1.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,11 @@ module Xolo
12
12
 
13
13
  module Helpers
14
14
 
15
- # Nightly cleanup of deprecated and skipped packages.
15
+ # Nightly maintenance tasks
16
+ # - accept lingering TEd EAs, if configured to
17
+ # - autorelease versions
18
+ # - clean up old versions
19
+ # - notify title maintainers of old unreleased pilots
16
20
  #
17
21
  # Also, alerts will be posted, and Emails will be sent to the
18
22
  # admins who added versions that have been in pilot for more than
@@ -29,8 +33,27 @@ module Xolo
29
33
  # Constants
30
34
  #####################################
31
35
 
32
- # At what hour should the nightly cleanup run?
33
- CLEANUP_HOUR = 2
36
+ # TODO: ? use the next 3 values as defaults, and
37
+ # set them in the server config
38
+
39
+ # how often does our maint timer check to see if it should run maintenance?
40
+ # Slightly less than an hour means there will always be at least one check every
41
+ # hour, sometimes two.
42
+ MAINT_CHECK_INTERVAL = 3500
43
+ # MAINT_CHECK_INTERVAL = 600
44
+
45
+ # At what hour should the nightly maintenance run?
46
+ MAINT_HOUR = 2
47
+ # MAINT_HOUR = 10
48
+
49
+ # Maint won't run unless the last run was at least this many
50
+ # seconds ago.
51
+ # 23 hrs ago means that even if we check twice within the
52
+ # MAINT_HOUR, only the first one will run.
53
+ MAINT_MAX_FREQ_SECS = 23 * 3600
54
+
55
+ # the route we POST to, to start the nightly process.
56
+ TIMED_MAINT_TRIGGER_ROUTE = '/maint/maint-internal'
34
57
 
35
58
  # on which day of the month should we send the unreleased pilot notifications?
36
59
  UNRELEASED_PILOTS_NOTIFICATION_DAY = 1
@@ -39,7 +62,7 @@ module Xolo
39
62
  # be automatically deleted this many days later.
40
63
  # If not set in the server config, this is
41
64
  # the default value.
42
- # use 0 or less to disable cleanup of deprecated versions
65
+ # use 0 or less to disable maintenance of deprecated versions
43
66
  DFT_DEPRECATED_LIFETIME_DAYS = 30
44
67
 
45
68
  # If a pilot has not been released in this many
@@ -55,71 +78,78 @@ module Xolo
55
78
  # Module Methods
56
79
  #####################################
57
80
 
58
- # A mutex for the cleanup process
59
- #
60
- # TODO: use Concrrent Ruby instead of Mutex
81
+ # A mutex for the maint process
61
82
  #
62
83
  # @return [Mutex] the mutex
63
84
  #####################
64
- def self.cleanup_mutex
65
- @cleanup_mutex ||= Mutex.new
85
+ def self.maint_mutex
86
+ @maint_mutex ||= Mutex.new
66
87
  end
67
88
 
68
- # nightly cleanup is done by a Concurrent::TimerTask, which checks every
89
+ # nightly maint is done by a Concurrent::TimerTask, which checks every
69
90
  # hour to see if it should do anything.
70
91
  #
71
- # It will only do the cleanup if the current time is in the 2am hour
72
- # (02:00 - 02:59)
92
+ # It will only do the maint if the current time is in the MAINT_HOUR hour
93
+ # (02:00 - 02:59 if MAINT_HOUR = 2)
73
94
  #
74
- # We trigger the cleanup by POSTing to /cleanup, so that it runs
95
+ # We trigger the maint by POSTing to TIMED_MAINT_TRIGGER_ROUTE, so that it runs
75
96
  # in the context of a request, having access to Title and Version instantiation.
76
97
  #
77
98
  # @return [Concurrent::TimerTask] the timed task to do log rotation
78
99
  ######################################
79
- def self.cleanup_timer_task
80
- return @cleanup_timer_task if @cleanup_timer_task
100
+ def self.maint_timer_task
101
+ return @maint_timer_task if @maint_timer_task
102
+
103
+ @maint_timer_task =
104
+ Concurrent::TimerTask.new(execution_interval: MAINT_CHECK_INTERVAL) { post_to_start_maint }
81
105
 
82
- @cleanup_timer_task =
83
- Concurrent::TimerTask.new(execution_interval: 3600) { post_to_start_cleanup }
106
+ Xolo::Server.logger.info "Created Concurrent::TimerTask for nightly maintenance. Will check every #{MAINT_CHECK_INTERVAL} secs, and run during the hour of: #{MAINT_HOUR}"
84
107
 
85
- Xolo::Server.logger.info 'Created Concurrent::TimerTask for nightly cleanup.'
86
- @cleanup_timer_task
108
+ @maint_timer_task
87
109
  end
88
110
 
89
- # When was our last cleanup?
90
- # @return [Time] the time of the last cleanup, or the epoch if never
111
+ # When was our last maint?
112
+ # @return [Time] the time of the last maint, or the epoch if never
91
113
  ######################################
92
- def self.last_cleanup
93
- @last_cleanup ||= Time.at(0)
114
+ def self.last_maint
115
+ @last_maint ||= Time.at(0)
94
116
  end
95
117
 
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
118
+ # Set the time of the last maint
119
+ # @param time [Time] the time of the last maint
120
+ # @return [Time] the time of the last maint
99
121
  ######################################
100
- def self.last_cleanup=(time)
101
- @last_cleanup = time
122
+ def self.last_maint=(time)
123
+ @last_maint = time
102
124
  end
103
125
 
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,
126
+ # post to the server to start the maint process
127
+ # This is done so that the maint can run in the context of a request,
106
128
  # having access to Title and Version instantiation.
107
129
  #
108
- # @param force [Boolean] force the cleanup to run now
130
+ # @param force [Boolean] force the maint to run now
109
131
  # @return [void]
110
132
  ######################################
111
- def self.post_to_start_cleanup(force: false)
133
+ def self.post_to_start_maint(force: false)
112
134
  if Xolo::Server.shutting_down?
113
- Xolo::Server.logger.info 'Not starting cleanup, server is shutting down'
135
+ Xolo::Server.logger.info 'Not starting maintenance, server is shutting down'
114
136
  return
115
137
  end
116
138
 
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)
139
+ # only run the maintenance if it's during the MAINT_HOUR
140
+ # and the last one was more than MAINT_MAX_FREQ_SECS ago
141
+ if force
142
+ Xolo::Server.logger.info 'Maint: Starting now due to force'
143
+
144
+ elsif Time.now.hour == MAINT_HOUR && (Time.now - last_maint) > MAINT_MAX_FREQ_SECS
145
+ Xolo::Server.logger.info 'Maint: Starting nightly run now'
146
+
147
+ else
148
+ Xolo::Server.logger.info "Maint: Not starting, it isn't time"
149
+ return
150
+ end
121
151
 
122
- uri = URI.parse "https://#{Xolo::Server::Helpers::Auth::IPV4_LOOPBACK}/maint/cleanup-internal"
152
+ uri = URI.parse "https://#{Xolo::Server::Helpers::Auth::IPV4_LOOPBACK}#{TIMED_MAINT_TRIGGER_ROUTE}"
123
153
  https = Net::HTTP.new(uri.host, uri.port)
124
154
  https.use_ssl = true
125
155
  # The server cert may be self-signed and/or doesn't
@@ -130,32 +160,38 @@ module Xolo
130
160
  request['Authorization'] = Xolo::Server::Helpers::Auth.internal_auth_token_header
131
161
 
132
162
  response = https.request(request)
133
- Xolo::Server.logger.info "Cleanup request response: #{response.code} #{response.body}"
163
+ Xolo::Server.logger.info "Maintenance request response: #{response.code} #{response.body}"
134
164
  end
135
165
 
136
- # Cleanup things that need to be cleaned up
166
+ # Perform maintenance tasks
137
167
  # @return [void]
138
168
  ################################
139
- def run_cleanup
169
+ def run_maint
140
170
  if Xolo::Server.shutting_down?
141
- log_info 'Cleanup: Not starting cleanup, server is shutting down'
171
+ log_info 'Maint: Not starting maint, server is shutting down'
142
172
  return
143
173
  end
144
- # TODO: Use Concurrent ruby rather than this instance variable
145
- mutex = Xolo::Server::Helpers::Maintenance.cleanup_mutex
174
+
175
+ mutex = Xolo::Server::Helpers::Maintenance.maint_mutex
146
176
 
147
177
  if mutex.locked?
148
- log_warn 'Cleanup: already running, skipping this run'
178
+ log_warn 'Maint: already running, skipping this run'
149
179
  return
150
180
  end
151
181
  mutex.lock
152
- log_info 'Cleanup: starting'
182
+ log_info 'Maint: starting'
183
+
184
+ # instantiate all the titles once now, rather than in all the task methods
185
+ title_objects_for_maint = Xolo::Server::Title.all_titles.map { |t| instantiate_title t }
153
186
 
154
- # add new cleanup tasks/methods here
155
- accept_title_editor_eas
156
- cleanup_versions
187
+ # add new maintenance tasks/methods here
188
+ accept_title_editor_eas title_objects_for_maint
189
+ auto_release_versions title_objects_for_maint
190
+ cleanup_versions title_objects_for_maint
191
+ notify_admins_of_unreleased_pilots title_objects_for_maint
157
192
 
158
- log_info 'Cleanup: complete'
193
+ Xolo::Server::Helpers::Maintenance.last_maint = Time.now
194
+ log_info 'Maint: complete'
159
195
  ensure
160
196
  mutex&.unlock
161
197
  end
@@ -164,38 +200,98 @@ module Xolo
164
200
  # and auto accept them if we need to
165
201
  # @return [void]
166
202
  ######################################
167
- def accept_title_editor_eas
203
+ def accept_title_editor_eas(title_objects)
168
204
  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'
205
+ log_debug 'Maint: The xolo server is not configured to auto-accept Title Editor EAs'
170
206
  return
171
207
  end
172
208
 
173
- log_info 'Cleanup: Looking for Title Editor EAs to auto-accept'
209
+ log_info 'Maint: Looking for Title Editor EAs to auto-accept'
174
210
 
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
211
+ title_objects.each do |title_obj|
212
+ title = title_obj.title
178
213
  next if title_obj.subscribed?
179
214
  next unless title_obj.jamf_patch_ea_awaiting_acceptance?
180
215
 
181
- log_info "Cleanup: Auto-accepting Title Editor EA for title '#{title}'"
216
+ log_info "Maint: Auto-accepting Title Editor EA for title '#{title}'"
182
217
  title_obj.accept_jamf_patch_ea_via_api
183
218
  rescue => e
184
- log_error "Cleanup: Error auto-accepting Title Editor EA for title '#{title}': #{e}"
219
+ log_error "Maint: Error auto-accepting Title Editor EA for title '#{title}': #{e}"
185
220
  end # Xolo::Server::Title.all_titles.each
186
221
 
187
- log_info 'Cleanup: Done with Title Editor EAs to auto-accept'
222
+ log_info 'Maint: Done with Title Editor EAs to auto-accept'
223
+ end
224
+
225
+ # Do any pending auto-releases
226
+ # @return [void]
227
+ ##############################
228
+ def auto_release_versions(title_objects)
229
+ log_info 'Maint: Auto-releasing appropriate versions'
230
+
231
+ today = Date.today
232
+
233
+ # Loop thru the titles
234
+ title_objects.each do |title_obj|
235
+ title = title_obj.title
236
+
237
+ # nothing to do if its nil or 'none'
238
+ next if title_obj.auto_release_delay.nil? || title_obj.auto_release_delay == Xolo::NONE
239
+
240
+ # title must be subscribed and autopkg
241
+ # (enforced in xadm)
242
+ next unless title_obj.subscribed? && !title_obj.autopkg_recipe.pix_empty?
243
+
244
+ # title must have a non-negative integer for auto_release_delay,
245
+ # (number of days to wait before release)
246
+ # could be zero. (enforced in xadm)
247
+ # NOTE: it is stored as a string because it might be 'none'
248
+ # TODO: store none as nil, so we don't have to do the pix_integer check??
249
+ delay = title_obj.auto_release_delay.to_i
250
+ next unless title_obj.auto_release_delay&.pix_integer? && !delay.negative?
251
+
252
+ # Any version with this creation date should be released now, skip if not
253
+ # So if the auto_release_delay is 7, its today - 7 days.
254
+ # if the auto_release_delay is 0, its today - 0 days, aka today
255
+ creation_date_to_be_released_today = today - delay
256
+
257
+ # Find the newest pilot version whose creation date is
258
+ # creation_date_to_be_released_today or earlier
259
+ # That one should be released now.
260
+ vobj_to_release = nil
261
+
262
+ # version objects are in order of newest to oldest
263
+ title_obj.version_objects.each do |vobj|
264
+ next unless vobj.pilot?
265
+ next unless vobj.creation_date.to_date <= creation_date_to_be_released_today
266
+
267
+ vobj_to_release = vobj
268
+ break
269
+ end
270
+
271
+ # no versions to release today
272
+ next unless vobj_to_release
273
+
274
+ # release it
275
+ log_debug "Maint: About to auto-release version '#{vobj_to_release.version}' of '#{title}' which came out #{vobj_to_release.creation_date}"
276
+
277
+ # releasing a version is done via the title obj, since it affects the status
278
+ # of all versions.
279
+ title_obj.release vobj_to_release.version
280
+
281
+ log_info "Maint: Auto-released version '#{vobj_to_release.version}' of '#{title}' which came out #{vobj_to_release.creation_date}", alert: true
282
+ end # each title
283
+
284
+ log_debug 'Maint: Done auto-releasing'
188
285
  end
189
286
 
190
287
  # Cleanup versions.
191
288
  # @return [void]
192
289
  ################################
193
- def cleanup_versions
194
- log_info 'Cleanup: cleaning up deprecated and skipped versions'
195
-
196
- Xolo::Server::Title.all_titles.each do |title|
197
- title_obj = instantiate_title title
290
+ def cleanup_versions(title_objects)
291
+ log_info 'Maint: Cleaning up deprecated and skipped versions'
198
292
 
293
+ # Loop thru the titles
294
+ title_objects.each do |title_obj|
199
295
  title_obj.version_objects.each do |version|
200
296
  if version.deprecated?
201
297
  cleanup_deprecated_version version
@@ -203,12 +299,8 @@ module Xolo
203
299
  cleanup_skipped_version version
204
300
  end # case
205
301
  end # each version
206
-
207
- notify_admins_of_unreleased_pilots(title_obj)
208
302
  end # each title
209
-
210
- Xolo::Server::Helpers::Maintenance.last_cleanup = Time.now
211
- log_info 'Cleanup: versions cleanup complete'
303
+ log_info 'Maint: Version cleanup complete'
212
304
  end
213
305
 
214
306
  # Cleanup a deprecated version.
@@ -223,7 +315,7 @@ module Xolo
223
315
  days_deprecated = (Time.now - version.deprecation_date) / 86_400
224
316
  return unless days_deprecated > deprecated_lifetime_days
225
317
 
226
- log_info "Cleanup: Deleting deprecated version '#{version.version}' of title '#{version.title}'"
318
+ log_info "Maint: Deleting deprecated version '#{version.version}' of title '#{version.title}'"
227
319
  version.delete
228
320
  end
229
321
 
@@ -234,38 +326,50 @@ module Xolo
234
326
  def cleanup_skipped_version(version)
235
327
  return if Xolo::Server.config.keep_skipped_versions
236
328
 
237
- log_info "Cleanup: Deleting skipped version '#{version.version}' of title '#{version.title}'"
329
+ log_info "Maint: Deleting skipped version '#{version.version}' of title '#{version.title}'"
238
330
  version.delete
239
331
  end
240
332
 
241
333
  # Notify the admins about unreleased pilots if needed
242
334
  # @return [void]
243
335
  ################################
244
- def notify_admins_of_unreleased_pilots(title_obj)
245
- return unless Time.now.day == UNRELEASED_PILOTS_NOTIFICATION_DAY
246
- return unless unreleased_pilots_notification_days.positive?
247
- return unless title_obj.latest_version
336
+ def notify_admins_of_unreleased_pilots(title_objects)
337
+ unless Time.now.day == UNRELEASED_PILOTS_NOTIFICATION_DAY
338
+ log_debug 'Maint: Not notifying admins of old pilots, wrong day of the month.'
339
+ return
340
+ end
341
+ unless unreleased_pilots_notification_days&.positive?
342
+ log_debug 'Maint: Not notifying admins of old pilots, unreleased_pilots_notification_days is not a positive integer'
343
+ return
344
+ end
345
+
346
+ log_info 'Maint: Notifying admins about old unreleased pilots'
248
347
 
249
- latest_vers_obj = instantiate_version title: title_obj, version: title_obj.latest_version
250
- return unless latest_vers_obj.pilot?
348
+ title_objects.each do |title_obj|
349
+ next unless title_obj.latest_version
251
350
 
252
- days_in_pilot = ((Time.now - latest_vers_obj.creation_date) / 86_400).to_i
351
+ latest_vers_obj = instantiate_version title: title_obj, version: title_obj.latest_version
352
+ next unless latest_vers_obj.pilot?
253
353
 
254
- return unless days_in_pilot > unreleased_pilots_notification_days
354
+ days_in_pilot = ((Time.now - latest_vers_obj.creation_date) / 86_400).to_i
255
355
 
256
- 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"
356
+ next unless days_in_pilot > unreleased_pilots_notification_days
257
357
 
258
- log_info alert_msg
259
- send_alert alert_msg
358
+ alert_msg = "Maint: Notifying #{title_obj.contact_email} about unreleased pilot '#{latest_vers}' of title '#{title_obj.title}', in pilot for #{days_in_pilot} days"
260
359
 
261
- email_msg = <<~MSG
360
+ log_info alert_msg
361
+ send_alert alert_msg
362
+
363
+ email_msg = <<~MSG
262
364
  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.
263
365
 
264
366
  To reduce clutter, please consider releasing it, deleting it, or deleting the whole title if it's no longer needed.
265
367
 
266
368
  If this is intentional, you can ignore this monthly message.
267
- MSG
268
- send_email to: title_obj.contact_email, subject: 'Unreleased Pilot Notification', msg: email_msg
369
+ MSG
370
+ send_email to: title_obj.contact_email, subject: 'Unreleased Pilot Notification', msg: email_msg
371
+ end # each title
372
+ log_info 'Maint: Done notifying admins about old unreleased pilots'
269
373
  end
270
374
 
271
375
  # how many days can a version be deprecated?
@@ -291,7 +395,7 @@ module Xolo
291
395
 
292
396
  progress "Server Shutdown by #{session[:admin]}", log: :info
293
397
 
294
- stop_cleanup_timer_task
398
+ stop_maint_timer_task
295
399
  stop_log_rotation_timer_task
296
400
  shutdown_pkg_deletion_pool
297
401
  wait_for_object_locks
@@ -316,12 +420,12 @@ module Xolo
316
420
  system "/bin/launchctl unload #{SERVER_LAUNCHD_PLIST}"
317
421
  end
318
422
 
319
- # Stop the cleanup timer task
423
+ # Stop the maintenance timer task
320
424
  # @return [void]
321
425
  ################################
322
- def stop_cleanup_timer_task
323
- progress 'Stopping the cleanup timer task', log: :info
324
- Xolo::Server::Helpers::Maintenance.cleanup_timer_task.shutdown
426
+ def stop_maint_timer_task
427
+ progress 'Stopping the maint timer task', log: :info
428
+ Xolo::Server::Helpers::Maintenance.maint_timer_task.shutdown
325
429
  end
326
430
 
327
431
  # Stop the log rotation timer task
@@ -174,16 +174,49 @@ module Xolo
174
174
  def stream_progress(stream_file:, stream:)
175
175
  log_debug "About to tail: usr/bin/tail -f -c +1 #{Shellwords.escape stream_file.to_s}"
176
176
 
177
- stdin, stdouterr, wait_thr = Open3.popen2e("/usr/bin/tail -f -c +1 #{Shellwords.escape stream_file.to_s}")
178
- stdin.close
179
-
180
- while line = stdouterr.gets
181
- break if line.chomp == Xolo::Server::PROGRESS_COMPLETE
182
-
183
- stream << line
184
- end
185
- stdouterr.close
186
- wait_thr.exit
177
+ begin
178
+ File.open(stream_file, 'r') do |file|
179
+ # Go straight to the end of the file if you only want new lines
180
+ file.seek(0, IO::SEEK_END)
181
+
182
+ heartbeat_counter = 0
183
+ loop do
184
+ # Check if the stream was closed by the user
185
+ break if stream.closed?
186
+
187
+ # Try to read the next line
188
+ # This won't block but will return nil if no line is ready
189
+ if line = file.gets
190
+ break if line.chomp == Xolo::Server::PROGRESS_COMPLETE
191
+
192
+ # Send the line to the browser immediately
193
+ stream << line
194
+ else
195
+ # If no new line, sleep for a fraction of a second
196
+ # This saves your computer's CPU from working too hard
197
+ sleep 0.1
198
+ heartbeat_counter += 1
199
+ if heartbeat_counter == 100 # every 10 secs
200
+ stream << ':heartbeat'
201
+ heartbeat_counter = 0
202
+ end
203
+ end # if
204
+ end # loop
205
+ end # file open
206
+ rescue IOError, Errno::ECONNRESET
207
+ nil
208
+ end # begin
209
+
210
+ # stdin, stdouterr, wait_thr = Open3.popen2e("/usr/bin/tail -f -c +1 #{Shellwords.escape stream_file.to_s}")
211
+ # stdin.close
212
+
213
+ # while line = stdouterr.gets
214
+ # break if line.chomp == Xolo::Server::PROGRESS_COMPLETE
215
+
216
+ # stream << line
217
+ # end
218
+ # stdouterr.close
219
+ # wait_thr.exit
187
220
  # TODO: deal with wait_thr.value.exitstatus > 0 ?
188
221
  end
189
222