xolo-server 2.2.4 → 2.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0af0f8b82fade2b7a8d3c65dedf4146e00e9adba56b7c553506a072b4942fcdb
4
- data.tar.gz: f351cb8a10515a30b851490f77b64067f9485783a5ea0522c0ddf739f5a2ea25
3
+ metadata.gz: d27f4a3e1656ef59731865488978113bcbeb4a5ebbea8c120585371c49aad335
4
+ data.tar.gz: a006f5ec49ad3cfbad6ae5908e2da17b65cdc2bf8eb0c3a7133fcaa0af6db3f4
5
5
  SHA512:
6
- metadata.gz: 9575c70693bbc8acf6abfb662e7d535a416dcb1bf32dfa69614194017011247ebcf8e4552f987952d484e30bca490100de0e311942eb9c18cf0bb2d7129ccb31
7
- data.tar.gz: 1a3ce31608e7fe9d62fe86465a8c8c686c6caec1d44062f2fce899e8e6ccc56ee353adf4b84ac834b8aa500d30c1a9bbe7e222400742609a1df3856131b87432
6
+ metadata.gz: c47f0964d7c5f5a42ddb909a0ee6b5d8bb01ead710b6be7fe9087e882081dcf753fc9f783be8bd0e5658b49df36d012f59f956db768026c915faabc8f5d9a4a8
7
+ data.tar.gz: 448ec9e58919ada9612c8651f814d168d4d6117c92b3e76faa2c5bdc74eb7747298f6bdebcc1547b7c662084d97fbbd7fb018331c8424f28b79bf1114cdacba7
data/data/client/xolo CHANGED
@@ -27,6 +27,9 @@ zmodload zsh/zutil
27
27
  ###############################
28
28
  ###############################
29
29
 
30
+ # just in case
31
+ export PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/libexec:$PATH"
32
+
30
33
  # Needed for the UTF-8 chars in the lsreg output
31
34
  export LANG="en_US.UTF-8"
32
35
  export LC_ALL="en_US.UTF-8"
@@ -94,6 +97,7 @@ be_verbose=
94
97
  debugging_on=
95
98
  no_versions=
96
99
  show_xolo_version=
100
+ do_recon=
97
101
 
98
102
  # arguments
99
103
  command=
@@ -152,7 +156,9 @@ Options:
152
156
  policies, etc, from test xolo server, not the production server.
153
157
  Setting the XOLO_TEST_MODE env. variable does the same thing.
154
158
  -r, --recon: With 'install' or 'uninstall' run a 'jamf recon' after the
155
- operation completes successfully.
159
+ operation completes successfully. With 'update', run a 'jamf recon'
160
+ before the checkin. NOTE: With update, the whole process may take
161
+ many minutes.
156
162
  -n, --no-versions: With the 'list-titles' and 'list-installed' commands,
157
163
  do not show the versions available, or the version
158
164
  installed, respectively.
@@ -187,6 +193,9 @@ Commands:
187
193
     every time this computer checks in with Jamf Pro.
188
194
  Note: this just runs the 'jamf policy' command, which will execute any pending
189
195
  Jamf policies triggered by "recurring check-in".
196
+ If --recon is given, a Jamf recon will run first, followed by a 1 minute pause
197
+ for Jamf to process the data before doing the check-in. This helps the Mac to see
198
+ very recently-released versions as patch updates.
190
199
 
191
200
  refresh, r
192
201
  Update xolo's information about current titles and versions.
@@ -356,42 +365,43 @@ function debug() {
356
365
  debugging_on && echo "DEBUG: $*" 1>&2
357
366
  }
358
367
 
359
- # Gather all installed bundle ids and installed versions from lsreg
368
+ # Gather all installed bundle ids and installed versions from system_profiler
360
369
  # store them in an associative array in $installed_apps.
361
370
  # keys are bundle ids, values are app name & installed version
362
371
  # separated by a semicolon
363
372
  # e.g. 'com.apple.Safari => Safari.app;14.0.3'
364
373
  ###############################
365
374
  function get_installed_apps() {
375
+ # debug "PATH is $PATH"
366
376
  debug "Getting all installed .apps on this Mac..."
367
377
  local app_name
368
378
  local app_bundle_id
369
379
  local app_vers
380
+ local app_plist
370
381
 
371
- # loop thru lsreg output
372
- "$LSREG" -dump Bundle | while IFS= read -r line ; do
373
- # a line if ------ starts a new record
374
- if [[ "$line" =~ '^-+$' ]] ; then
375
- unset app_name
376
- unset app_bundle_id
377
- unset app_vers
378
- continue
379
- fi
382
+ json_app_data=$(/usr/sbin/system_profiler -json SPApplicationsDataType )
383
+
384
+ apps_to_look_at=$(echo "$json_app_data" | grep '"path"' | cut -d'"' -f4 | grep -v '^/System/')
385
+
386
+ for app_path in ${(f)apps_to_look_at} ; do
387
+ app_name=$(/usr/bin/basename "$app_path")
388
+ app_plist="$app_path/Contents/Info.plist"
389
+
390
+ if [[ -f "$app_plist" ]] ; then
391
+ app_bundle_id=$(/usr/bin/plutil -extract CFBundleIdentifier raw "$app_plist" 2>/dev/null)
380
392
 
381
- # $MATCH is the whole matched string, $match is the array of captures
382
- # NOTE this only works if the path is listed before the CFBundleIdentifier
383
- # which seems to be the case in the lsreg output
384
- [[ "$line" =~ '^path:[[:space:]]+/.*/(.+\.app) \(' ]] && app_name=$match[1]
385
- [[ "$line" =~ '^[[:space:]]+CFBundleIdentifier = \"(.+)\";' ]] && app_bundle_id=$match[1]
386
- [[ "$line" =~ '^[[:space:]]+CFBundleShortVersionString = \"(.+)\";' ]] && app_vers=$match[1]
393
+ app_vers=$(/usr/bin/plutil -extract CFBundleShortVersionString raw "$app_plist" 2>/dev/null)
394
+
395
+ # Use CFBundleVersion if no CFBundleShortVersionString
396
+ [[ -z "$app_vers" ]] && \
397
+ app_vers=$(/usr/bin/plutil -extract CFBundleVersion raw "$app_plist" 2>/dev/null)
387
398
 
388
- if [[ -n "$app_name" && -n "$app_bundle_id" && -n "$app_vers" ]] ; then
389
399
  installed_apps[$app_bundle_id]="$app_name;$app_vers"
390
- unset app_name
391
- unset app_bundle_id
392
- unset app_vers
400
+ else
401
+ debug "App Path '$app_path' has no Info.plist"
393
402
  fi
394
403
  done
404
+
395
405
  # installed_apps is now an associative array with bundle ids as keys and app names as values
396
406
 
397
407
  # print the installed apps - this is a long list
@@ -402,8 +412,70 @@ function get_installed_apps() {
402
412
  done
403
413
  fi
404
414
 
415
+ # debug "PATH is $PATH"
405
416
  } # end installed_apps
406
417
 
418
+ ########################################
419
+ ########################################
420
+ ########################################
421
+ ########################################
422
+ ########################################
423
+ ########################################
424
+ # Gather all installed bundle ids and installed versions from lsreg
425
+ # store them in an associative array in $installed_apps.
426
+ # keys are bundle ids, values are app name & installed version
427
+ # separated by a semicolon
428
+ # e.g. 'com.apple.Safari => Safari.app;14.0.3'
429
+ ###############################
430
+ # function get_installed_apps() {
431
+ # debug "Getting all installed .apps on this Mac..."
432
+ # local app_name
433
+ # local app_bundle_id
434
+ # local app_vers
435
+
436
+ # # loop thru lsreg output
437
+ # "$LSREG" -dump Bundle | while IFS= read -r line ; do
438
+ # # a line if ------ starts a new record
439
+ # if [[ "$line" =~ '^-+$' ]] ; then
440
+ # unset app_name
441
+ # unset app_bundle_id
442
+ # unset app_vers
443
+ # continue
444
+ # fi
445
+
446
+ # # $MATCH is the whole matched string, $match is the array of captures
447
+ # # NOTE this only works if the path is listed before the CFBundleIdentifier
448
+ # # which seems to be the case in the lsreg output
449
+ # [[ "$line" =~ '^path:[[:space:]]+/.*/(.+\.app) \(' ]] && app_name=$match[1]
450
+ # [[ "$line" =~ '^[[:space:]]+CFBundleIdentifier = \"(.+)\";' ]] && app_bundle_id=$match[1]
451
+ # [[ "$line" =~ '^[[:space:]]+CFBundleShortVersionString = \"(.+)\";' ]] && app_vers=$match[1]
452
+
453
+ # if [[ -n "$app_name" && -n "$app_bundle_id" && -n "$app_vers" ]] ; then
454
+ # installed_apps[$app_bundle_id]="$app_name;$app_vers"
455
+ # unset app_name
456
+ # unset app_bundle_id
457
+ # unset app_vers
458
+ # fi
459
+ # done
460
+ # # installed_apps is now an associative array with bundle ids as keys and app names as values
461
+
462
+ # # print the installed apps - this is a long list
463
+ # if debugging_on ; then
464
+ # debug "All installed .apps on this Mac:"
465
+ # for bundle app in ${(kv)installed_apps}; do
466
+ # debug "'$bundle' -> '$app'"
467
+ # done
468
+ # fi
469
+
470
+ # } # end installed_apps
471
+ ########################################
472
+ ########################################
473
+ ########################################
474
+ ########################################
475
+ ########################################
476
+ ########################################
477
+ ########################################
478
+
407
479
  # Extract data from the client-data.json file
408
480
  #
409
481
  # TODO: Once we only support macOS 15 (Sequoia) and higher, we
@@ -463,7 +535,7 @@ function all_xolo_titles() {
463
535
  read -r -d '' jscode <<ENDJAVASCRIPT
464
536
  result = Object.keys(parsed_data.titles).join('\n');
465
537
  ENDJAVASCRIPT
466
- extract_json_data "$jscode" | sort
538
+ extract_json_data "$jscode" | /usr/bin/sort
467
539
  }
468
540
 
469
541
  # Outputs all known titles, one per line, with versions and statuses
@@ -969,6 +1041,12 @@ function run_recon() {
969
1041
  function update() {
970
1042
  say "Updating installed titles, or installing newly scoped ones..."
971
1043
 
1044
+ if [[ -n "$do_recon" ]] ; then
1045
+ run_recon
1046
+ say "Pausing to let Jamf Pro process recon data..."
1047
+ sleep 60
1048
+ fi
1049
+
972
1050
  debug "Running jamf policy..."
973
1051
  if be_verbose ; then
974
1052
  $JAMF policy -verbose
@@ -1020,12 +1098,15 @@ function list_installed_titles() {
1020
1098
  local ttl
1021
1099
  local app_data
1022
1100
 
1023
-
1024
1101
  verbose "Locating installed titles..."
1025
1102
 
1026
1103
  # populate the installed_apps associative array
1027
1104
  get_installed_apps
1028
1105
 
1106
+ # gather output here, display it at the end
1107
+ # rather than interspersed with debug lines
1108
+ installed_titles_report=""
1109
+
1029
1110
  while IFS= read -r ttl; do
1030
1111
  app_data=$(app_data_for_title $ttl)
1031
1112
 
@@ -1036,6 +1117,8 @@ function list_installed_titles() {
1036
1117
  display_title_if_installed_by_version_script $ttl
1037
1118
  fi
1038
1119
  done <<<"$(all_xolo_titles)"
1120
+
1121
+ say "$installed_titles_report"
1039
1122
  return 0
1040
1123
  }
1041
1124
 
@@ -1075,9 +1158,9 @@ function display_title_if_installed_by_app_data() {
1075
1158
  [[ "$inst_app_name" == "$app_name" ]] || return
1076
1159
 
1077
1160
  if [[ -n "$no_versions" ]] ; then
1078
- echo $ttl
1161
+ installed_titles_report="$installed_titles_report\n$ttl"
1079
1162
  else
1080
- echo "$ttl ($inst_app_vers)"
1163
+ installed_titles_report="$installed_titles_report\n$ttl ($inst_app_vers)"
1081
1164
  fi
1082
1165
  }
1083
1166
 
@@ -1114,9 +1197,9 @@ function display_title_if_installed_by_version_script() {
1114
1197
  [[ -n "$app_vers" ]] || return
1115
1198
 
1116
1199
  if [[ -n "$no_versions" ]] ; then
1117
- echo $ttl
1200
+ installed_titles_report="$installed_titles_report\n$ttl"
1118
1201
  else
1119
- echo "$ttl ($app_vers)"
1202
+ installed_titles_report="$installed_titles_report\n$ttl ($inst_app_vers)"
1120
1203
  fi
1121
1204
  }
1122
1205
 
@@ -12,7 +12,7 @@ module Xolo
12
12
 
13
13
  module Version
14
14
 
15
- VERSION = '2.2.4'.freeze
15
+ VERSION = '2.4.0'.freeze
16
16
 
17
17
  end
18
18
 
@@ -156,6 +156,7 @@ module Xolo
156
156
 
157
157
  # @!attribute server_admin_jamf_group
158
158
  # @return [String] The name of a Jamf account-group containing users of 'xadm'
159
+ # who are allowed to manage the xoloserver itself
159
160
  server_admin_jamf_group: {
160
161
  type: :string,
161
162
  desc: <<~ENDDESC
@@ -35,6 +35,11 @@ module Xolo
35
35
  AUTOPKG_UPLOADED_BY = 'autopkg'
36
36
  FAIL_UNTRUSTED_RECIPES_CLI_OPT = '-k FAIL_RECIPES_WITHOUT_TRUST_INFO=yes'
37
37
 
38
+ # when we find a .pkg after a recipe has run, it can't be older than this many
39
+ # seconds, or we assume it is from some other run of the recipe, not the one we
40
+ # just did. Currently 20 min
41
+ MAX_PKG_AGE = 1200
42
+
38
43
  # Module Methods
39
44
  #######################
40
45
  #######################
@@ -127,7 +132,9 @@ module Xolo
127
132
  clean_old_pkgs pkgdir
128
133
 
129
134
  cmd = autopkg_run_command(title_object)
130
- progress "Running AutoPkg recipe for #{title_object.title} via command: #{cmd.join(' ')}", log: :info
135
+ progress "Running AutoPkg recipe '#{recipe}' for #{title_object.title}..."
136
+
137
+ log_info "Running AutoPkg recipe for #{title_object.title} via command: #{cmd.join(' ')}"
131
138
 
132
139
  unlock_autopkg_user_keychain
133
140
 
@@ -135,20 +142,83 @@ module Xolo
135
142
  souterr.strip!
136
143
 
137
144
  if status.success?
138
- progress "AutoPkg recipe #{recipe} for title '#{title_object.title}' completed successfully.", log: :info, alert: true
145
+ progress "AutoPkg recipe '#{recipe}' for title '#{title_object.title}' completed successfully.", log: :info, alert: true
139
146
  log_debug 'AutoPkg output:'
140
147
  souterr.lines.each { |l| log_debug "AutoPkg: #{l.chomp}" }
141
148
 
142
149
  pkgs = pkgdir.children.select { |c| c.extname == '.pkg' }
150
+ # the Pathname of the newest .pkg
143
151
  pkgs.max_by(&:mtime)
144
152
 
153
+ # Failure is a option
145
154
  else
146
- progress "ERROR: AutoPkg recipe #{recipe} failed with status #{status.exitstatus}.", log: :error, alert: true
147
- log_error 'AutoPkg output:'
148
- souterr.lines.each { |l| log_error "AutoPkg: #{l.chomp}" }
155
+ handle_failed_recipe(
156
+ recipe: recipe,
157
+ title_object: title_object,
158
+ status: status,
159
+ souterr: souterr
160
+ )
161
+ end
162
+ end
163
+
164
+ # Deal with a failed recipe run
165
+ #
166
+ ###############################
167
+ def handle_failed_recipe(recipe:, title_object:, status:, souterr:)
168
+ errmsg = "ERROR: AutoPkg recipe '#{recipe}' for title '#{title_object.title}' failed with status #{status.exitstatus}. Details logged and emailed to '#{title_object.contact_email}'"
169
+
170
+ progress errmsg, log: :error, alert: true
171
+
172
+ # log the details
173
+ log_error 'AutoPkg output:'
174
+ souterr.lines.each { |l| log_error "AutoPkg: #{l.chomp}" }
175
+
176
+ # email the details to the contact addr for the title
177
+ email_subj = "ERROR: AutoPkg recipe FAILED for title '#{title_object.title}'"
178
+ email_msg = <<~ENDEMAIL
179
+ The autopkg recipe '#{recipe}' for title '#{title_object.title}' failed with status #{status.exitstatus}.
180
+
181
+ Please check that the recipe is up to date. If not, please update the recipe's repo by running this as the autppkg user on the xolo server:
182
+
183
+ autopkg repo-update <repo-name>
184
+
185
+ Then confirm you still trust the recipe.
186
+ If so, run:
187
+
188
+ autopkg update-trust-info '#{recipe}'
189
+
190
+ Then tell xolo the xolo server to re-run the recipe - the .pkg will always be assigned to the latest version.
191
+
192
+ xadm rerun-autopkg-recipe #{title_object.title}
193
+
194
+ If needed, contact your xoloserver administrator to perform the autopkg commands.
195
+
196
+ Here is the output from the failed recipe:
197
+ ------------------------------------------
198
+ #{souterr}
199
+ ENDEMAIL
200
+
201
+ send_email to: title_object.contact_email, subject: email_subj, msg: email_msg, html: false
202
+
203
+ raise "AutoPkg recipe #{recipe} failed."
204
+ end
205
+
206
+ # confirm a given autopkg package is OK to use after running a recipe
207
+ ###########################
208
+ def validate_autopkg_pkg(pkg_src)
209
+ if pkg_src.nil?
210
+ msg = 'AutoPkg recipe is enabled for this title, but no pkg was found after running the recipe. Please check the AutoPkg recipe and the server log for details.'
211
+ progress msg, log: :warn, alert: true
212
+ return false
213
+ end
149
214
 
150
- raise "AutoPkg recipe #{recipe} failed."
215
+ oldest_allowed = Time.now - MAX_PKG_AGE # 20 minutes ago
216
+ if pkg_src.mtime < oldest_allowed
217
+ msg = "AutoPkg recipe is enabled for this title, and a pkg was found after running the recipe, but it was last modified at #{pkg_src.mtime}, which is more than 20 minutes ago. To avoid accidentally uploading an old pkg, the server will not upload this pkg. Please check the AutoPkg recipe and the server log for details."
218
+ progress msg, log: :warn, alert: true
219
+ return false
151
220
  end
221
+ true
152
222
  end
153
223
 
154
224
  # remove any existing pkgs from the given autopkg output dir
@@ -108,14 +108,14 @@ module Xolo
108
108
  @maint_timer_task
109
109
  end
110
110
 
111
- # When was our last maint?
111
+ # When was our last automated maint?
112
112
  # @return [Time] the time of the last maint, or the epoch if never
113
113
  ######################################
114
114
  def self.last_maint
115
115
  @last_maint ||= Time.at(0)
116
116
  end
117
117
 
118
- # Set the time of the last maint
118
+ # Set the time of the last maint, if it was automated
119
119
  # @param time [Time] the time of the last maint
120
120
  # @return [Time] the time of the last maint
121
121
  ######################################
@@ -153,7 +153,7 @@ module Xolo
153
153
  Xolo::Server.logger.info 'Maint: Starting nightly run now'
154
154
 
155
155
  else
156
- Xolo::Server.logger.info "Maint: Not starting, it isn't time"
156
+ Xolo::Server.logger.debug "Maint: Not starting, it isn't time"
157
157
  return
158
158
  end
159
159
 
@@ -195,13 +195,16 @@ module Xolo
195
195
 
196
196
  # add new maintenance tasks/methods here
197
197
  ######
198
+ update_ssl_cert_and_key
198
199
  accept_title_editor_eas title_objects_for_maint
199
200
  auto_release_versions title_objects_for_maint
200
201
  cleanup_versions title_objects_for_maint
201
202
  notify_admins_of_unreleased_pilots title_objects_for_maint
202
203
 
203
- # make note of the time.
204
- Xolo::Server::Helpers::Maintenance.last_maint = Time.now
204
+ # make note of the time, but only for automated runs.
205
+ # Manual runs don't affect this.
206
+ Xolo::Server::Helpers::Maintenance.last_maint = Time.now unless @manual_maintenance
207
+
205
208
  log_info 'Maint: complete'
206
209
 
207
210
  log_debug 'Maint: running update_client_data after all tasks'
@@ -210,6 +213,51 @@ module Xolo
210
213
  mutex&.unlock
211
214
  end
212
215
 
216
+ # Re-fetch the SSL certificate and key, as defined in the server config,
217
+ # and compare to the current values. If they are different, save and activate
218
+ # the new ones.
219
+ #
220
+ # NOTE, to get the current values we read the active files from disk,
221
+ # not the value in memory in the Xolo::Server.config object.
222
+ # In other words we use this:
223
+ # curr_crt = Xolo::Server::Configuration::SSL_CERT_FILE.read.strip
224
+ # Not this:
225
+ # curr_crt = Xolo::Server.config.ssl_cert
226
+ # This is because the file contents are what get sent to the https clients,
227
+ # so that's what matters.
228
+ # We do however update the in-memory value if an update is needed, just in case.
229
+ ######################################
230
+ def update_ssl_cert_and_key
231
+ log_info 'Maint: Checking if SSL Certificate & Key need updating.'
232
+ update = false
233
+
234
+ # check the cert
235
+ new_crt_src = Xolo::Server.config.raw_data[:ssl_cert]
236
+ log_debug "Maint: Re-fetching SSL Certificate from: #{new_crt_src}"
237
+ new_crt = Xolo::Server.config.data_from_command_file_or_string(new_crt_src).strip
238
+ curr_crt = Xolo::Server::Configuration::SSL_CERT_FILE.read.strip
239
+ update = true unless new_crt == curr_crt
240
+
241
+ # check the key
242
+ new_key_src = Xolo::Server.config.raw_data[:ssl_key]
243
+ log_debug "Maint: Re-fetching SSL Private Key from: #{new_key_src}"
244
+ new_key = Xolo::Server.config.data_from_command_file_or_string(new_key_src).strip
245
+ curr_key = Xolo::Server::Configuration::SSL_KEY_FILE.read.strip
246
+ update = true unless new_key == curr_key
247
+
248
+ if update
249
+ log_info 'Maint: SSL Certificate or Key has been updated, installing...'
250
+ Xolo::Server.config.ssl_cert = new_crt
251
+ Xolo::Server.config.ssl_key = new_key
252
+ Xolo::Server::Configuration::SSL_CERT_FILE.pix_save new_crt
253
+ Xolo::Server::Configuration::SSL_KEY_FILE.pix_save new_key
254
+ else
255
+ log_debug 'Maint: SSL Certificate and Private Key are unchanged.'
256
+ end
257
+
258
+ log_info 'Maint: Done Checking if SSL Certificate & Key.'
259
+ end
260
+
213
261
  # look for any titles that need their Title Editor EA's accepted,
214
262
  # and auto accept them if we need to
215
263
  # @return [void]
@@ -286,13 +334,13 @@ module Xolo
286
334
  next unless vobj_to_release
287
335
 
288
336
  # release it
289
- log_debug "Maint: About to auto-release version '#{vobj_to_release.version}' of '#{title}' which came out #{vobj_to_release.creation_date}"
337
+ log_debug "Maint: About to auto-release '#{title}' version '#{vobj_to_release.version}' which came out #{vobj_to_release.creation_date}"
290
338
 
291
339
  # releasing a version is done via the title obj, since it affects the status
292
340
  # of all versions.
293
341
  title_obj.release vobj_to_release.version
294
342
 
295
- log_info "Maint: Auto-released version '#{vobj_to_release.version}' of '#{title}' which came out #{vobj_to_release.creation_date}", alert: true
343
+ log_info "Maint: Auto-released '#{title}' version '#{vobj_to_release.version}' which came out #{vobj_to_release.creation_date}", alert: true
296
344
 
297
345
  # pause for server to catch up
298
346
  sleep 60
@@ -91,7 +91,7 @@ module Xolo
91
91
  new_version: new_version
92
92
  )
93
93
  else
94
- log_debug "Title '#{title_name}' ID #{title_id} is not a subscribed title in Xolo. Ignoring webhook."
94
+ log_debug "Title '#{title_name}' ID #{title_id} is not a subscribed title. Ignoring webhook."
95
95
  end
96
96
  rescue => e
97
97
  msg = "Error processing PatchSoftwareTitleUpdated webhook event: #{e.class}: #{e}"
@@ -101,21 +101,6 @@ module Xolo
101
101
  end # thread
102
102
  end
103
103
 
104
- # TODO: Delete this method after confirming we don't want it
105
- # It wasn't in use when it was commented out.
106
- #
107
- # Subscribe to a title on a given patch source.
108
- # @param source_id [Integer] the id or name of the patch source in Jamf
109
- # @param name_id [Integer] the name_id of the title on that patch source
110
- # @param display_name [String] an display name for the title
111
- # @return [Integer] the id of the new subscribed title, or false on failure
112
- #####################################
113
- # def subscribe_to_title(source_id:, name_id:, display_name:)
114
- # new_sub = Jamf::PatchTitle.create name: display_name, source_id: source_id, name_id: name_id, cnx: jamf_cnx
115
-
116
- # new_sub.save
117
- # end
118
-
119
104
  end # Subscriptions
120
105
 
121
106
  end # Helpers
@@ -1808,6 +1808,8 @@ module Xolo
1808
1808
  pol.self_service_description = new_desc
1809
1809
 
1810
1810
  pol.self_service_install_button_text = Xolo::Server::Title::SELF_SERVICE_INSTALL_BTN_TEXT
1811
+ pol.self_service_reinstall_button_text = Xolo::Server::Title::SELF_SERVICE_REINSTALL_BTN_TEXT
1812
+
1811
1813
  # icon already uploaded to jamf
1812
1814
  return if ssvc_icon_id
1813
1815
 
@@ -1823,13 +1825,6 @@ module Xolo
1823
1825
  self.ssvc_icon_id = Jamf::Policy.fetch(id: pol.id, cnx: jamf_cnx).icon.id
1824
1826
  end
1825
1827
 
1826
- # run the autopkg recipe if defined
1827
- # See Helpers::AutoPkg for more details
1828
- ############################
1829
- def run_autopkg_recipe
1830
- server_app_instance.run_autopkg_recipe self
1831
- end
1832
-
1833
1828
  # Methods used by subscription stuff
1834
1829
  ##############################
1835
1830
 
@@ -119,17 +119,17 @@ module Xolo
119
119
 
120
120
  # the capabilites
121
121
  stub_patch.capabilities.add_criterion(
122
- # Use the title as the name, so it matches all the future versions/patches
123
- # avoiding a warning message in the TEd gui.
124
- # name: Xolo::Server::Version::STUB_PATCH_CAPABILITY_CRITERION_NAME,
125
- name: title,
122
+ name: Xolo::Server::Version::STUB_PATCH_CAPABILITY_CRITERION_NAME,
126
123
  operator: Xolo::Server::Version::STUB_PATCH_CAPABILITY_CRITERION_OPERATOR,
127
124
  value: Xolo::Server::Version::STUB_PATCH_CAPABILITY_CRITERION_VALUE
128
125
  )
129
126
 
130
127
  # the component
131
128
  stub_patch.add_component(
132
- name: Xolo::Server::Version::STUB_PATCH_COMPONENT_NAME,
129
+ # Use the title as the name, so it matches all the future versions/patches
130
+ # avoiding a warning message in the TEd gui.
131
+ # name: Xolo::Server::Version::STUB_PATCH_COMPONENT_NAME,
132
+ name: title,
133
133
  version: Xolo::Server::Version::STUB_PATCH_VERSION
134
134
  )
135
135
 
@@ -51,7 +51,7 @@ module Xolo
51
51
  start_time: Xolo::Server.start_time,
52
52
  uptime: uptime_secs.pix_humanize_secs,
53
53
  uptime_secs: uptime_secs,
54
- last_maint: Xolo::Server::Helpers::Maintenance.last_maint,
54
+ last_automated_maint: Xolo::Server::Helpers::Maintenance.last_maint,
55
55
  app_env: Xolo::Server.app_env,
56
56
  test_server: Xolo::Server.test_server?,
57
57
  data_dir: Xolo::Server::DATA_DIR,
@@ -71,6 +71,14 @@ module Xolo
71
71
  }
72
72
 
73
73
  if params[:extended]
74
+ ssl_cert_data ||= OpenSSL::X509::Certificate.new(Xolo::Server::Configuration::SSL_CERT_FILE.read)
75
+ state[:ssl_cert_subject] = ssl_cert_data.subject
76
+ state[:ssl_cert_issuer] = ssl_cert_data.issuer
77
+ state[:ssl_cert_valid_from] = ssl_cert_data.not_before.localtime
78
+ state[:ssl_cert_expires] = ssl_cert_data.not_after.localtime
79
+ state[:ssl_cert_serial] = ssl_cert_data.serial
80
+ state[:ssl_cert_signature_algorithm] = ssl_cert_data.signature_algorithm
81
+
74
82
  state[:gem_path] = Gem.paths.path
75
83
  state[:load_path] = $LOAD_PATH
76
84
  state[:object_locks] = Xolo::Server.object_locks
@@ -98,6 +106,8 @@ module Xolo
98
106
  # run the maintenance process manually from a server admin via xadm
99
107
  ################
100
108
  post '/maint/maint-start' do
109
+ @manual_maintenance = true
110
+
101
111
  log_info "Starting manual server maintenance by #{session[:admin]}"
102
112
 
103
113
  session[:admin] = "Maintenance by #{session[:admin]}"
@@ -288,6 +288,30 @@ module Xolo
288
288
  body data
289
289
  end
290
290
 
291
+ # rerun the titles autopkg recipe
292
+ ######################
293
+ post '/titles/:title/rerun_autopkg_recipe' do
294
+ halt_on_missing_title params[:title]
295
+ halt_on_locked_title params[:title]
296
+ title = instantiate_title params[:title]
297
+
298
+ unless title.autopkg_enabled?
299
+ msg = "Title '#{params[:title]}' is not configured to use AutoPkg"
300
+ log_debug "ERROR: #{msg}"
301
+ halt 409, { status: 409, error: msg }
302
+ end
303
+
304
+ unless title.latest_version
305
+ msg = "Title '#{params[:title]}' has no versions, cannot re-run the recipe"
306
+ log_debug "ERROR: #{msg}"
307
+ halt 404, { status: 404, error: msg }
308
+ end
309
+
310
+ with_streaming do
311
+ title.rerun_autopkg_recipe
312
+ end
313
+ end
314
+
291
315
  end # Titles
292
316
 
293
317
  end # Routes
@@ -120,6 +120,7 @@ module Xolo
120
120
  JPAPI_PATCH_REPORT_RSRC = 'patch-report'
121
121
 
122
122
  SELF_SERVICE_INSTALL_BTN_TEXT = 'Install'
123
+ SELF_SERVICE_REINSTALL_BTN_TEXT = 'Re-Install'
123
124
 
124
125
  # Class Methods
125
126
  ######################
@@ -1144,6 +1145,27 @@ module Xolo
1144
1145
  unlock
1145
1146
  end
1146
1147
 
1148
+ # run the autopkg recipe if defined
1149
+ # See Helpers::AutoPkg for more details
1150
+ ############################
1151
+ def run_autopkg_recipe
1152
+ server_app_instance.run_autopkg_recipe self
1153
+ end
1154
+
1155
+ # Re run the autopkg recipe
1156
+ # and re-upload the pkg
1157
+ ############################
1158
+ def rerun_autopkg_recipe
1159
+ pkg_src = server_app_instance.run_autopkg_recipe self
1160
+
1161
+ return unless server_app_instance.validate_autopkg_pkg pkg_src
1162
+
1163
+ latest_version_obj = version_object latest_version
1164
+ latest_version_obj.pkg_is_from_autopkg = true
1165
+
1166
+ server_app_instance.process_and_upload_autopkg_pkg title, latest_version_obj, pkg_src
1167
+ end
1168
+
1147
1169
  # Is this title locked for updates?
1148
1170
  #############################
1149
1171
  def locked?
@@ -748,18 +748,7 @@ module Xolo
748
748
 
749
749
  pkg_src = title_object.run_autopkg_recipe
750
750
 
751
- if pkg_src.nil?
752
- msg = 'AutoPkg recipe is enabled for this title, but no pkg was found after running the recipe. Please check the AutoPkg recipe and the server log for details.'
753
- progress msg, log: :warn, alert: true
754
- return
755
- end
756
-
757
- oldest_allowed = Time.now - 1200 # 20 minutes ago
758
- if pkg_src.mtime < oldest_allowed
759
- msg = "AutoPkg recipe is enabled for this title, and a pkg was found after running the recipe, but it was last modified at #{pkg_src.mtime}, which is more than 20 minutes ago. To avoid accidentally uploading an old pkg, the server will not upload this pkg. Please check the AutoPkg recipe and the server log for details."
760
- progress msg, log: :warn, alert: true
761
- return
762
- end
751
+ return unless server_app_instance.validate_autopkg_pkg pkg_src
763
752
 
764
753
  # this lets future code know that the pkg we're working with came from autopkg
765
754
  self.pkg_is_from_autopkg = true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xolo-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.4
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Lasell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday