xolo-server 2.3.0 → 2.5.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: e78577a11f1d65aba560ec5540c3a79acdfc7d6a491060e256bb9e2da5a057d9
4
- data.tar.gz: 9e7e776f48e8e9e260bc41a9090603bbfc41d1dad7dd62fc00396ce9914450cb
3
+ metadata.gz: 7f515836aab0143c5343395e240de91947be9aef7cc70b266d4e1a84eb5fe714
4
+ data.tar.gz: 7f438c26d4974cb273241ec6bd72a9521eef562283cea9f64ba5f4521c34cf40
5
5
  SHA512:
6
- metadata.gz: b83f99bda3666a2fd124241a26882499c393a75b3289fb503bdcb6ac994827228b5f813acc79a7a81f437f879b2e9aaca727800ddd711f17b2713d7a45a98738
7
- data.tar.gz: 05bb86ec68010bf2258853983f093936ffa735c52144b6760c64177621a971bdf93823a46562a4b347c5a080d5206900a39ff8b6019e7fb1fa0cf0b400e4336e
6
+ metadata.gz: 2b2cc84027b9a9af0fc4619d1ba5d564700043207e5ee96be50a69f248b5b4644605b5de569cb9906ec6b71c3d7ee322041bfe4395187cdcce06bcacf9b2d099
7
+ data.tar.gz: 344224658c124b01d6caa0b175ef64e3e200618335224119efb2d5430a38e178d15bba1878282ee16908c51c9979d5dbadbcd02da8aa293812ac22a1a4d32598
data/data/client/xolo CHANGED
@@ -97,6 +97,7 @@ be_verbose=
97
97
  debugging_on=
98
98
  no_versions=
99
99
  show_xolo_version=
100
+ do_recon=
100
101
 
101
102
  # arguments
102
103
  command=
@@ -155,7 +156,9 @@ Options:
155
156
  policies, etc, from test xolo server, not the production server.
156
157
  Setting the XOLO_TEST_MODE env. variable does the same thing.
157
158
  -r, --recon: With 'install' or 'uninstall' run a 'jamf recon' after the
158
- 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.
159
162
  -n, --no-versions: With the 'list-titles' and 'list-installed' commands,
160
163
  do not show the versions available, or the version
161
164
  installed, respectively.
@@ -190,6 +193,9 @@ Commands:
190
193
     every time this computer checks in with Jamf Pro.
191
194
  Note: this just runs the 'jamf policy' command, which will execute any pending
192
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.
193
199
 
194
200
  refresh, r
195
201
  Update xolo's information about current titles and versions.
@@ -1035,6 +1041,12 @@ function run_recon() {
1035
1041
  function update() {
1036
1042
  say "Updating installed titles, or installing newly scoped ones..."
1037
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
+
1038
1050
  debug "Running jamf policy..."
1039
1051
  if be_verbose ; then
1040
1052
  $JAMF policy -verbose
@@ -459,15 +459,17 @@ module Xolo
459
459
  },
460
460
 
461
461
  # @!attribute auto_release_delay
462
- # @return [String] 'none' or an integer in string. How many days before new versions are automatically released?
462
+ # @return [String] 'none', 'monthly', or an integer in string.
463
+ # How many days before new versions are automatically released?
463
464
  # Applies only to subscribed titles with autopkg recipes.
464
465
  auto_release_delay: {
465
466
  label: 'Automatic Release Delay',
466
467
  cli: :A,
467
468
  type: :string,
468
469
  walkthru_na: :auto_release_delay_na,
470
+ validate: true,
469
471
  changelog: true,
470
- invalid_msg: "Must be a non-negative integer, or '#{Xolo::NONE}'.",
472
+ invalid_msg: "Must be a non-negative integer, '#{Xolo::MONTHLY}', or '#{Xolo::NONE}'.",
471
473
  desc: <<~ENDDESC
472
474
  Specify how many days after a new version appears before it is automatically released from pilot.
473
475
 
@@ -477,6 +479,8 @@ module Xolo
477
479
 
478
480
  Setting it to zero means that new versions will be released at the next nightly auto-release task. *** USE WITH EXTREME CAUTION ***
479
481
 
482
+ Setting it to '#{Xolo::MONTHLY}' will release it on whatever day of the month is defined in the Xolo server's 'auto_release_monthly' setting, or on the first day of the month if the server isn't set. The server may be set to a specific date, like '15' or to an ordinal-weekday, like '3rd Mon'.
483
+
480
484
  To unset, meaning versions are never auto-released, use '#{Xolo::NONE}'
481
485
 
482
486
  Auto-release happens during the server's nightly maintenance between 2-3am.
@@ -23,6 +23,9 @@ module Xolo
23
23
 
24
24
  ERROR = 'ERROR'
25
25
 
26
+ # A possible value for a title's auto-release delay
27
+ MONTHLY = 'monthly'
28
+
26
29
  # Several things use x
27
30
  X = 'x'
28
31
 
@@ -12,7 +12,7 @@ module Xolo
12
12
 
13
13
  module Version
14
14
 
15
- VERSION = '2.3.0'.freeze
15
+ VERSION = '2.5.0'.freeze
16
16
 
17
17
  end
18
18
 
@@ -143,6 +143,25 @@ module Xolo
143
143
  ENDDESC
144
144
  },
145
145
 
146
+ # @!attribute xadm_authenticator
147
+ # @return [String] How to authenticate users of 'xadm', either 'use-jamf' or a path to an authentication tool.
148
+ xadm_authenticator: {
149
+ required: true,
150
+ type: :string,
151
+ desc: <<~ENDDESC
152
+ The method for authenticating users of 'xadm'. The value 'use-jamf' will authenticate the user's name and password by trying
153
+ to connect to the Jamf Pro API. This may not work for Jamf Cloud instances with an identity provider or other more complex
154
+ authentication processes. There are plans to address this in a future version of xolo.
155
+
156
+ Alternatively, provide the full path to a CLI tool installed on the xolo server. This tool must take two lines on its standard-input:
157
+ the username, then the password. It must return 'OK' to its standard-output, and exit with status 0, for the credentials to be
158
+ considered correct.
159
+
160
+ After authentication, the username provided must be a member of the Jamf account-group defined in 'admin_jamf_group', and/or
161
+ that defined in 'server_admin_jamf_group'.
162
+ ENDDESC
163
+ },
164
+
146
165
  # @!attribute admin_jamf_group
147
166
  # @return [String] The name of a Jamf account-group containing users of 'xadm'
148
167
  admin_jamf_group: {
@@ -156,6 +175,7 @@ module Xolo
156
175
 
157
176
  # @!attribute server_admin_jamf_group
158
177
  # @return [String] The name of a Jamf account-group containing users of 'xadm'
178
+ # who are allowed to manage the xoloserver itself
159
179
  server_admin_jamf_group: {
160
180
  type: :string,
161
181
  desc: <<~ENDDESC
@@ -829,6 +849,30 @@ module Xolo
829
849
  NOTE: AutoPkg recipes are always run with '-k FAIL_RECIPES_WITHOUT_TRUST_INFO=yes', and will fail if you have not 'trusted'
830
850
  them by making an override. See the AutoPkg docs for details.
831
851
  ENDDESC
852
+ },
853
+
854
+ # @!attribute auto_release_monthly
855
+ # @return [String] Which day of the month should auto-releases happen?
856
+ auto_release_monthly: {
857
+ type: :string,
858
+ desc: <<~ENDDESC
859
+ If a title has an --auto-release-delay set to '#{Xolo::MONTHLY}', this defines which day of the month the release happens. Releases happen during nightly maintenence, between 2 and 3am, which may affect the value you put here. Monthly releases will only release the newest version, if it's status is 'pilot'.
860
+
861
+ Setting this string to an integer will cause the release to happen on that day of the month, so '15' makes it happen on the 15th of the month. If you set the value to beyond the end of the month, it will happen on the last day of the month, so set it to '31' (or higher) for the last day of every month.
862
+
863
+ Alternatively you can set it to a string specifying an 'ordinal-weekday', e.g. '2nd Tue', which will cause the release to happen on the second Tuesday of every month.
864
+
865
+ Ordinal weekday strings must be two space-separated values: First one of '1st', '2nd', '3rd', '4th', or 'last', then a 3-letter English weekday, one of 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', or 'Sat'. All of these are case-insensitive.
866
+
867
+ Examples: '2nd Sun', '4th Fri', 'Last Wed'
868
+
869
+ If a title is set to 'monthly', and the server has no valid value here, releases happen on the first day of every month, regardless of weekday.
870
+
871
+ Setting monthly auto-release can help avoid update-fatigue when you have lots of titles auto-releasing frequently. Users can expect all their updates at around the same time once a month.
872
+
873
+ Note that new versions of titles can always be manually released at any time once they are added to xolo.
874
+ So if an important security update comes out and you don't want to wait up to a month to release it, you can do so manually with 'xadm release <title> <version>'
875
+ ENDDESC
832
876
  }
833
877
 
834
878
  }.freeze
@@ -59,6 +59,9 @@ module Xolo
59
59
  # The loopback address for IPV4, aka 'localhost'
60
60
  IPV4_LOOPBACK = '127.0.0.1'
61
61
 
62
+ # This is a possible value for xadm_authenticator
63
+ USE_JAMF = 'use-jamf'
64
+
62
65
  # Module methods
63
66
  #####################
64
67
  #####################
@@ -239,6 +242,33 @@ module Xolo
239
242
  false
240
243
  end
241
244
 
245
+ # authenticate via an external xadm authenticator tool.
246
+ # it must exit with status 0, and send 'OK' to its stdout
247
+ #
248
+ # @param admin [String] The jamf acct name of the person seeking access
249
+ #
250
+ # @param pw [String] The password for the jamf acct
251
+ #
252
+ # @return [Boolean] Did the password work for the user?
253
+ ####################
254
+ def authenticated_via_xadm_authenticator?(admin, pw)
255
+ upw = "#{admin}\n#{pw}"
256
+ out = nil
257
+ err = nil
258
+ success = false
259
+
260
+ Open3.popen3(Xolo::Server.config.xadm_authenticator) do |inp, outp, serr, wthr|
261
+ inp.print upw
262
+ inp.close
263
+
264
+ out = outp.read.chomp
265
+ err = serr.read.chomp
266
+ success = wthr.value.success?
267
+ end
268
+ log_warn "xadm authenticator failed for admin '#{admin}': #{err} " unless success
269
+ success && out == Xolo::OK
270
+ end
271
+
242
272
  # Try to authenticate the jamf user trying to log in to xolo.
243
273
  # This must be a regular Jamf user acct, not an API client.
244
274
  #
@@ -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
  #######################
@@ -89,12 +94,53 @@ module Xolo
89
94
  @autopkg_enabled = false
90
95
  end
91
96
 
97
+ # the autopkg report-plist path, based on the
98
+ # title name & timestamp w/nanosecs
99
+ # @return [Pathname]
100
+ ##############################
101
+ def autopkg_report_plist_path
102
+ @autopkg_report_plist_path ||= Pathname.new "/tmp/xolo-autopkg-report-#{@title_id_for_autopkg}-#{Time.now.strftime('%s%N')}.plist"
103
+ end
104
+
105
+ # Given a Hash or Array, recursively look for a
106
+ # Hash Key :version and return the value.
107
+ # Returns the first such key it finds.
108
+ #
109
+ # @param collection [Hash, Array] the data to search
110
+ #
111
+ # @return [String, nil] the first 'version' value found, or nil if not found
112
+ ###########################
113
+ def find_version(collection)
114
+ version = nil
115
+
116
+ if collection.is_a? Hash
117
+ collection.each do |key, value|
118
+ if key == :version
119
+ version = value
120
+ break
121
+ end
122
+ version = find_version(value) if value.is_a?(Hash) || value.is_a?(Array)
123
+ break if version
124
+ end
125
+ elsif collection.is_a? Array
126
+ collection.each do |value|
127
+ version = find_version(value) if value.is_a?(Hash) || value.is_a?(Array)
128
+ break if version
129
+ end
130
+ end # if collection.is_a
131
+
132
+ version
133
+ end # def
134
+
92
135
  # the autopkg run command for this title
93
136
  #####################################
94
137
  def autopkg_run_command(title_object)
95
138
  # is autopkg enabled for the server as a whole?
96
139
  return unless autopkg_enabled?
97
140
 
141
+ # is autopkg enabled for this title?
142
+ return unless title_object.autopkg_enabled?
143
+
98
144
  [
99
145
  '/bin/launchctl',
100
146
  'asuser',
@@ -106,7 +152,9 @@ module Xolo
106
152
  'run',
107
153
  '--verbose',
108
154
  title_object.autopkg_recipe,
109
- FAIL_UNTRUSTED_RECIPES_CLI_OPT
155
+ FAIL_UNTRUSTED_RECIPES_CLI_OPT,
156
+ '--report-plist',
157
+ autopkg_report_plist_path.to_s
110
158
  ]
111
159
  end
112
160
 
@@ -122,33 +170,115 @@ module Xolo
122
170
  # is autopkg enabled for this title?
123
171
  return unless title_object.autopkg_enabled?
124
172
 
173
+ # Make this available to other methods here
174
+ @title_id_for_autopkg = title_object.title
175
+
125
176
  recipe = title_object.autopkg_recipe
126
177
  pkgdir = Pathname.new title_object.autopkg_dir
127
178
  clean_old_pkgs pkgdir
128
179
 
129
180
  cmd = autopkg_run_command(title_object)
130
- progress "Running AutoPkg recipe for #{title_object.title} via command: #{cmd.join(' ')}", log: :info
181
+ progress "Running AutoPkg recipe '#{recipe}' for #{title_object.title}..."
182
+ log_info "Running AutoPkg recipe for #{title_object.title} via command: #{cmd.join(' ')}"
131
183
 
132
184
  unlock_autopkg_user_keychain
133
-
134
185
  souterr, status = Open3.capture2e(*cmd)
135
186
  souterr.strip!
136
187
 
137
188
  if status.success?
138
- progress "AutoPkg recipe #{recipe} for title '#{title_object.title}' completed successfully.", log: :info, alert: true
189
+ msg = "AutoPkg recipe '#{recipe}' for title '#{title_object.title}' completed successfully."
190
+ progress msg, log: :info, alert: true
191
+
139
192
  log_debug 'AutoPkg output:'
140
193
  souterr.lines.each { |l| log_debug "AutoPkg: #{l.chomp}" }
141
194
 
142
195
  pkgs = pkgdir.children.select { |c| c.extname == '.pkg' }
196
+ # the Pathname of the newest .pkg
143
197
  pkgs.max_by(&:mtime)
144
198
 
199
+ # Failure is a option
145
200
  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}" }
201
+ handle_failed_recipe(
202
+ recipe: recipe,
203
+ title_object: title_object,
204
+ status: status,
205
+ souterr: souterr
206
+ )
207
+ end
208
+ end
209
+
210
+ # Deal with a failed recipe run
211
+ #
212
+ ###############################
213
+ def handle_failed_recipe(recipe:, title_object:, status:, souterr:)
214
+ errmsg = "ERROR: AutoPkg recipe '#{recipe}' for title '#{title_object.title}' failed with status #{status.exitstatus}. Details logged and emailed to '#{title_object.contact_email}'"
215
+
216
+ progress errmsg, log: :error, alert: true
149
217
 
150
- raise "AutoPkg recipe #{recipe} failed."
218
+ # log the details
219
+ log_error 'AutoPkg output:'
220
+ souterr.lines.each { |l| log_error "AutoPkg: #{l.chomp}" }
221
+
222
+ # email the details to the contact addr for the title
223
+ email_subj = "ERROR: AutoPkg recipe FAILED for title '#{title_object.title}'"
224
+ email_msg = <<~ENDEMAIL
225
+ The autopkg recipe '#{recipe}' for title '#{title_object.title}' failed with status #{status.exitstatus}.
226
+
227
+ 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:
228
+
229
+ autopkg repo-update <repo-name>
230
+
231
+ Then confirm you still trust the recipe.
232
+ If so, run:
233
+
234
+ autopkg update-trust-info '#{recipe}'
235
+
236
+ Then tell xolo the xolo server to re-run the recipe - the .pkg will always be assigned to the latest version.
237
+
238
+ xadm rerun-autopkg-recipe #{title_object.title}
239
+
240
+ If needed, contact your xoloserver administrator to perform the autopkg commands.
241
+
242
+ Here is the output from the failed recipe:
243
+ ------------------------------------------
244
+ #{souterr}
245
+ ENDEMAIL
246
+
247
+ send_email to: title_object.contact_email, subject: email_subj, msg: email_msg, html: false
248
+
249
+ raise "AutoPkg recipe #{recipe} failed."
250
+ end
251
+
252
+ # confirm a given autopkg package is OK to use after running a recipe
253
+ ###########################
254
+ def validate_autopkg_pkg(pkg_src, desired_version)
255
+ if pkg_src.nil?
256
+ msg = 'No pkg was found after running the autopkg recipe. Please check the AutoPkg recipe and the server log for details. Make sure the --autopkg-dir is correct. Nothing to upload.'
257
+ progress msg, log: :warn, alert: true
258
+ return false
151
259
  end
260
+
261
+ oldest_allowed = Time.now - MAX_PKG_AGE # 20 minutes ago
262
+ if pkg_src.mtime < oldest_allowed
263
+ msg = "A pkg was found after running the autopkg 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. Nothing to upload"
264
+ progress msg, log: :warn, alert: true
265
+ return false
266
+ end
267
+
268
+ fetched_version =
269
+ if autopkg_report_plist_path.file?
270
+ json_report = `/usr/bin/plutil -convert json #{autopkg_report_plist_path.pix_shellescape} -o -`
271
+ report = JSON.parse json_report, symbolize_names: true
272
+ find_version report[:summary_results]
273
+ end
274
+
275
+ if fetched_version != desired_version
276
+ msg = "A pkg was found after running the recipe, but the version that was fetched was '#{fetched_version}', and we were expecting version '#{desired_version}'. Nothing to upload"
277
+ progress msg, log: :warn, alert: true
278
+ return false
279
+ end
280
+
281
+ true
152
282
  end
153
283
 
154
284
  # remove any existing pkgs from the given autopkg output dir
@@ -43,14 +43,15 @@ module Xolo
43
43
  # MAINT_CHECK_INTERVAL = 600
44
44
 
45
45
  # At what hour should the nightly maintenance run?
46
+ # default to 2am
46
47
  MAINT_HOUR = 2
47
48
  # MAINT_HOUR = 10
48
49
 
49
- # Maint won't run unless the last run was at least this many
50
+ # Automated maint won't run unless the last run was at least this many
50
51
  # seconds ago.
51
52
  # 23 hrs ago means that even if we check twice within the
52
53
  # MAINT_HOUR, only the first one will run.
53
- MAINT_MAX_FREQ_SECS = 23 * 3600
54
+ AUTO_MAINT_MAX_FREQ_SECS = 23 * 3600
54
55
 
55
56
  # the route we POST to, to start the nightly process.
56
57
  TIMED_MAINT_TRIGGER_ROUTE = '/maint/maint-internal'
@@ -145,11 +146,11 @@ module Xolo
145
146
  now = Time.now
146
147
 
147
148
  # only run the maintenance if it's during the MAINT_HOUR
148
- # and the last one was more than MAINT_MAX_FREQ_SECS ago
149
+ # and the last one was more than AUTO_MAINT_MAX_FREQ_SECS ago
149
150
  if force
150
151
  Xolo::Server.logger.info 'Maint: Starting now due to force'
151
152
 
152
- elsif now.hour == MAINT_HOUR && (now - last_maint) > MAINT_MAX_FREQ_SECS
153
+ elsif now.hour == MAINT_HOUR && (now - last_maint) > AUTO_MAINT_MAX_FREQ_SECS
153
154
  Xolo::Server.logger.info 'Maint: Starting nightly run now'
154
155
 
155
156
  else
@@ -295,60 +296,157 @@ module Xolo
295
296
  # Loop thru the titles
296
297
  title_objects.each do |title_obj|
297
298
  title = title_obj.title
299
+ vobj_to_release = nil
298
300
 
299
- # nothing to do if its nil or 'none'
301
+ # nothing to do if auto_release_delay is nil or 'none'
300
302
  next if title_obj.auto_release_delay.nil? || title_obj.auto_release_delay == Xolo::NONE
301
303
 
302
304
  # title must be subscribed and autopkg
303
- # (enforced in xadm)
305
+ # (enforced in xadm also)
304
306
  next unless title_obj.subscribed? && !title_obj.autopkg_recipe.pix_empty?
305
307
 
306
- # title must have a non-negative integer for auto_release_delay,
307
- # (number of days to wait before release)
308
- # could be zero. (enforced in xadm)
309
- # NOTE: it is stored as a string because it might be 'none'
310
- # TODO: store none as nil, so we don't have to do the pix_integer check??
311
- delay = title_obj.auto_release_delay.to_i
312
- next unless title_obj.auto_release_delay&.pix_integer? && !delay.negative?
313
-
314
- # Any version with this creation date should be released now, skip if not
315
- # So if the auto_release_delay is 7, its today - 7 days.
316
- # if the auto_release_delay is 0, its today - 0 days, aka today
317
- creation_date_to_be_released_today = today - delay
318
-
319
- # Find the newest pilot version whose creation date is
320
- # creation_date_to_be_released_today or earlier
321
- # That one should be released now.
322
- vobj_to_release = nil
323
-
324
- # version objects are in order of newest to oldest
325
- title_obj.version_objects.each do |vobj|
326
- next unless vobj.pilot?
327
- next unless vobj.creation_date.to_date <= creation_date_to_be_released_today
308
+ # if the title is set for monthly release, is today the
309
+ # right day of the month?
310
+ if title_obj.auto_release_delay == Xolo::MONTHLY
311
+ unless monthly_release_today?
312
+ log_debug "Title '#{title}' is set to monthly, but today isn't '#{Xolo::Server.config.auto_release_monthly}'"
313
+ next
314
+ end
315
+
316
+ # For Monthlies, we only ever auto-release the newest version,
317
+ # and only if it is in pilot
318
+ latest_vobj = title_obj.version_objects.first
319
+ unless latest_vobj&.pilot?
320
+ log_debug "Title '#{title}' is set to monthly, but the latest version isn't in pilot"
321
+ next
322
+ end
323
+
324
+ vobj_to_release = latest_vobj
325
+
326
+ # Look for a version to be released today.
327
+ # The delay value must be a non-negative integer
328
+ elsif title_obj.auto_release_delay.pix_integer?
329
+ delay = title_obj.auto_release_delay.to_i
330
+ if delay.negative?
331
+ log_debug "Title '#{title}': invalid auto_release_delay: #{delay}"
332
+ next
333
+ end
334
+
335
+ # Given the title's delay, what creation date means
336
+ # we should release today?
337
+ # So if the auto_release_delay is 7, its today - 7 days.
338
+ # if the auto_release_delay is 0, its today - 0 days, aka today
339
+ creation_date_to_be_released_today = today - delay
340
+
341
+ # Find the newest pilot version whose creation date is
342
+ # creation_date_to_be_released_today or earlier
343
+ # That one should be released now.
344
+ # version_objects are in order of newest to oldest
345
+ title_obj.version_objects.each do |vobj|
346
+ # gotta be in pilot
347
+ next unless vobj.pilot?
348
+
349
+ # creation date has to be appropriate
350
+ next unless vobj.creation_date.to_date <= creation_date_to_be_released_today
351
+
352
+ vobj_to_release = vobj
353
+ break
354
+ end # title_obj.version_objects.each
355
+ end # if title_obj.auto_release_delay == Xolo::MONTHLY
328
356
 
329
- vobj_to_release = vobj
330
- break
357
+ # no versions to release today
358
+ unless vobj_to_release
359
+ log_debug "Title '#{title}': nothing to auto-release"
360
+ next
331
361
  end
332
362
 
333
- # no versions to release today
334
- next unless vobj_to_release
363
+ # must have a jamf pkg file uploaded
364
+ # (won't if autopkg recipe failed)
365
+ next if vobj_to_release.jamf_pkg_file.pix_empty?
335
366
 
336
367
  # release it
337
- log_debug "Maint: About to auto-release version '#{vobj_to_release.version}' of '#{title}' which came out #{vobj_to_release.creation_date}"
368
+ log_debug "Maint: About to auto-release '#{title}' version '#{vobj_to_release.version}' which came out #{vobj_to_release.creation_date}"
338
369
 
339
- # releasing a version is done via the title obj, since it affects the status
340
- # of all versions.
370
+ # releasing a version is done via the title obj,
371
+ # since it affects the status of all versions.
341
372
  title_obj.release vobj_to_release.version
342
373
 
343
- log_info "Maint: Auto-released version '#{vobj_to_release.version}' of '#{title}' which came out #{vobj_to_release.creation_date}", alert: true
374
+ log_info "Maint: Auto-released '#{title}' version '#{vobj_to_release.version}' which came out #{vobj_to_release.creation_date}", alert: true
344
375
 
345
376
  # pause for server to catch up
346
- sleep 60
347
- end # each title
377
+ sleep 30
378
+ end # title_objects.each do |title_obj|
348
379
 
349
380
  log_debug 'Maint: Done auto-releasing'
350
381
  end
351
382
 
383
+ # Is today the day for a monthly release?
384
+ # The order we check:
385
+ # - If the server is set to an ordinal weekday, like '3rd Thu'
386
+ # - If the server is set to the last weekday, like 'Last Thu'
387
+ # - If the server is set to a specific day of the month, which may be beyond the last day
388
+ # - All other cases, the first of the month.
389
+ #
390
+ # @return [Boolean] is today the day for a monthly release?
391
+ #################################
392
+ def monthly_release_today?
393
+ return @monthly_release_today if defined? @monthly_release_today
394
+
395
+ today = Date.today
396
+ last_day_of_this_month = Date.new(today.year, today.month, -1)
397
+
398
+ # downcased string...
399
+ server_setting = Xolo::Server.config.auto_release_monthly.to_s.downcase
400
+
401
+ @monthly_release_today =
402
+ # matches, e.g. '2nd Mon'
403
+ ##########
404
+ if today.to_time.pix_ordinal_weekday.casecmp? server_setting
405
+ true
406
+
407
+ # matches 'last <wkday>' e.g. 'last mon'
408
+ ##########
409
+ elsif server_setting.start_with? 'last '
410
+ # e.g. 'Mmon'
411
+ release_weekday = server_setting.split(' ').last
412
+
413
+ # is it the right weekday?
414
+ if today.strftime('%a').casecmp? release_weekday
415
+ # yes... so is it the last one? Make a Range and see it today is in it.
416
+ last_week_of_the_month = (last_day_of_this_month - 6)..last_day_of_month
417
+ last_week_of_the_month.include? today
418
+
419
+ # it is not the right weekday
420
+ else
421
+ false
422
+ end
423
+
424
+ # matches an integer day of the month, or the last day of the month
425
+ # if server_setting is greater than the number of days in the month
426
+ ##########
427
+ elsif server_setting.pix_integer? && server_setting.to_i.positive?
428
+ # convert string to integer
429
+ server_setting = server_setting.to_i
430
+
431
+ # integer for the last day of the month
432
+ last_day = last_day_of_this_month.day
433
+
434
+ # use the last day of this month if the server is set beyond it
435
+ server_setting = last_day if server_setting >= last_day
436
+
437
+ # is it today?
438
+ server_setting == today.day
439
+
440
+ # All other cases, e.g. bad value or unset on the server,
441
+ # ... the first of the month.
442
+ ##########
443
+ else
444
+ today.day == 1
445
+ end
446
+
447
+ @monthly_release_today
448
+ end
449
+
352
450
  # Cleanup versions.
353
451
  # @return [void]
354
452
  ################################
@@ -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. Ignoring webhook."
94
+ log_debug "Title '#{title_name}' ID #{title_id} is not a subscribed title on this Xolo server. Ignoring webhook."
95
95
  end
96
96
  rescue => e
97
97
  msg = "Error processing PatchSoftwareTitleUpdated webhook event: #{e.class}: #{e}"
@@ -101,6 +101,39 @@ module Xolo
101
101
  end # thread
102
102
  end
103
103
 
104
+ # Manually re-trigger a the latest version,
105
+ # if the subscription update failed.
106
+ #
107
+ # Will delete and re-create the latest version, which
108
+ # must be pilot or pending.
109
+ def rerun_subscription_update(title)
110
+ log_debug "Re-running subscription update for latest version of #{title}"
111
+
112
+ title_obj = instantiate_title title
113
+ raise Xolo::UnsupportedError, "Title #{title} is not a subscribed title." unless title_obj.subscribed?
114
+
115
+ version = title_obj.latest_version
116
+ vobj = instantiate_version title: title, version: version
117
+ unless %w[pilot pending].include? vobj.status
118
+ raise Xolo::UnsupportedError,
119
+ "Latest version must be 'pending' or 'pilot' to rerun subscription updates. Version '#{version}' of title '#{title}' is '#{vobj.status}' "
120
+ end
121
+
122
+ title_obj.log_change msg: "Re-running subscription update for latest version' #{version}'"
123
+
124
+ progress "Deleting existing version '#{version}'...", log: :info
125
+ vobj.delete
126
+ progress 'Pausing for package deletion (sorry for delay)...'
127
+
128
+ sleep 60
129
+
130
+ progress "Re-adding version '#{version}' via subscription...", log: :info
131
+ Xolo::Server::Version.add_version_via_subscription(
132
+ title_object: title_obj,
133
+ new_version: version
134
+ )
135
+ end
136
+
104
137
  end # Subscriptions
105
138
 
106
139
  end # Helpers
@@ -156,7 +156,6 @@ module Xolo
156
156
 
157
157
  update_description_in_jamf
158
158
  update_ssvc
159
- # TODO: deal with icon changes: if changes_for_update&.key? :self_service_icon
160
159
 
161
160
  # are there any versions yet? if no, not active
162
161
  if jamf_title_active?
@@ -234,6 +233,7 @@ module Xolo
234
233
 
235
234
  vers_obj.update_jamf_package_notes(ttl_obj: self) if need_to_update_description?
236
235
  vers_obj.update_patch_policy_deployment(ttl_obj: self) if changes_for_update&.key? :self_service_updates
236
+ vers_obj.set_patch_policy_ssvc_icon if self_service_updates?
237
237
  end
238
238
  end
239
239
 
@@ -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
 
@@ -454,7 +454,7 @@ module Xolo
454
454
  msg = "Jamf: Starting deletion of Package '#{jamf_pkg_name}' id #{jamf_pkg_id} at #{Time.now.strftime '%F %T'}"
455
455
  progress msg, log: :info
456
456
 
457
- warning = "IMPORTANT: Package deletion can be slow. If you plan to re-add this version, '#{version}', please\nwait at least 5 minutes before doing so."
457
+ warning = "IMPORTANT: Package deletion can be slow. If you plan to re-add this version, '#{version}', please wait a few minutes before doing so."
458
458
  progress warning
459
459
 
460
460
  self.class.pkg_deletion_pool.post do
@@ -978,7 +978,8 @@ module Xolo
978
978
  #
979
979
  # @param ttl_obj [Xolo::Server::Title] the title object that is being changed.
980
980
  ###############################
981
- def update_patch_policy_deployment(ttl_obj:)
981
+ def update_patch_policy_deployment(ttl_obj: nil)
982
+ ttl_obj ||= title_object
982
983
  return unless ttl_obj.changes_for_update[:self_service_updates]
983
984
 
984
985
  ppol = jamf_patch_policy
@@ -1033,6 +1034,7 @@ module Xolo
1033
1034
  ppol.self_service_reminder_frequency = PATCH_POL_SSVC_REMINDER_FREQ_HRS
1034
1035
  ppol.deadline = PATCH_POL_SSVC_DEADLINE_DAYS
1035
1036
 
1037
+ set_patch_policy_ssvc_icon ppol
1036
1038
  # Automatic deployment
1037
1039
  else
1038
1040
  progress "Jamf: Configuring Patch Policy for version '#{version}' of title '#{title}' for automatic deployment", log: :info
@@ -1052,23 +1054,27 @@ module Xolo
1052
1054
  # @param icon_id [Integer, String] The id of the existing icon to use.
1053
1055
  # @return [Void]
1054
1056
  ##########################
1055
- def set_patch_policy_ssvc_icon(ppol)
1057
+ def set_patch_policy_ssvc_icon(ppol = nil)
1056
1058
  return unless use_ssvc_updates?
1057
1059
 
1060
+ ppol ||= jamf_patch_policy
1061
+
1058
1062
  if title_object.ssvc_icon_id
1059
1063
  icon_id = title_object.ssvc_icon_id
1060
1064
  log_debug "Jamf: ssvc_icon_id from title object is: #{icon_id}"
1061
1065
  else
1062
1066
  icon_id = title_object.jamf_manual_install_released_policy.self_service_icon.id
1063
1067
  log_debug "Jamf: ssvc_icon_id from manual install policy is: #{icon_id}"
1064
-
1068
+ title_object.ssvc_icon_id = icon_id
1065
1069
  end
1066
1070
 
1067
1071
  if icon_id.pix_empty?
1068
- log_debug 'Jamf: could not find ssvc icon id, not setting icon for patch policy.'
1072
+ progress "Jamf: could not find ssvc icon id for title '#{title}', not setting icon for patch policy of version '#{version}'.", log: :info
1069
1073
  return
1070
1074
  end
1071
1075
 
1076
+ progress "Jamf: Setting icon for Self Service Updates of version '#{version}' of title '#{title}'", log: :info
1077
+
1072
1078
  # this str can't be frozen for now
1073
1079
  icon_xml = +<<~ENDXML
1074
1080
  <?xml version="1.0" encoding="UTF-8"?>
@@ -1081,12 +1087,14 @@ module Xolo
1081
1087
  </patch_policy>
1082
1088
  ENDXML
1083
1089
 
1084
- log_debug "Jamf: Adding existing SSvc icon to patch policy for version '#{version}' of title '#{title}'"
1090
+ log_debug "Jamf: Adding existing SSvc icon id '#{icon_id}' to patch policy '#{ppol.id}' for version '#{version}' of title '#{title}'"
1085
1091
 
1086
1092
  # TODO: in ruby-jss, update c_put and related methods to
1087
1093
  # use unfrozen dups of strings for #gsub
1088
1094
 
1089
1095
  jamf_cnx.c_put "patchpolicies/id/#{ppol.id}", icon_xml
1096
+ rescue Jamf::NoSuchItemError => e
1097
+ log_error "Couldn't set SSvc icon id '#{icon_id}' to patch policy '#{ppol.id}' for version '#{version}' of title '#{title}': Jamf::NoSuchItemError: #{e}"
1090
1098
  end
1091
1099
 
1092
1100
  # @return [String] the URL for the Jamf Pro Patch Policy that updates to this version
@@ -42,9 +42,12 @@ module Xolo
42
42
  pw = payload[:password]
43
43
 
44
44
  err =
45
- if !member_of_admin_jamf_group?(admin)
46
- "'#{admin}' is not allowed to use the Xolo server"
47
- elsif !authenticated_via_jamf?(admin, pw)
45
+ ("'#{admin}' is not allowed to use the Xolo server" unless member_of_admin_jamf_group?(admin))
46
+
47
+ err ||=
48
+ if Xolo::Server.config.xadm_authenticator == Xolo::Server::Helpers::Auth::USE_JAMF
49
+ "Incorrect xolo admin username or password for '#{admin}'" unless authenticated_via_jamf?(admin, pw)
50
+ elsif !authenticated_via_xadm_authenticator?(admin, pw)
48
51
  "Incorrect xolo admin username or password for '#{admin}'"
49
52
  end
50
53
 
@@ -117,6 +117,21 @@ module Xolo
117
117
  body resp
118
118
  end
119
119
 
120
+ # Manually re-trigger a previous, failed subscription update
121
+ # Will delete and re-create the existing version, which
122
+ # must be pilot or pending.
123
+ ###############
124
+ post '/titles/:title/rerun-subscription' do
125
+ title = params[:title]
126
+
127
+ halt_on_missing_title title
128
+ halt_on_locked_title title
129
+
130
+ with_streaming do
131
+ rerun_subscription_update(title)
132
+ end
133
+ end
134
+
120
135
  end # Module
121
136
 
122
137
  end # Routes
@@ -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
  ######################
@@ -490,7 +491,8 @@ module Xolo
490
491
  # @return [Pathname] The the local file containing the self-service icon
491
492
  #####################
492
493
  def ssvc_icon_file
493
- @ssvc_icon_file ||= self.class.ssvc_icon_file title
494
+ # dont cache this, read it every time
495
+ self.class.ssvc_icon_file title
494
496
  end
495
497
 
496
498
  # @return [Pathname] The the local file containing the code of the version script
@@ -984,16 +986,23 @@ module Xolo
984
986
  # @return [void]
985
987
  ######################################
986
988
  def validate_release(version_to_release)
989
+ # already released
987
990
  if released_version == version_to_release
988
991
  raise Xolo::InvalidDataError,
989
992
  "Version '#{version_to_release}' of title '#{title}' is already released"
990
993
  end
991
994
 
995
+ # no such version
992
996
  unless versions.include? version_to_release
993
997
  raise Xolo::NoSuchItemError,
994
998
  "No version '#{version_to_release}' for title '#{title}'"
995
999
  end
996
1000
 
1001
+ # missing .pkg file
1002
+ vobj = version_object version_to_release
1003
+ raise Xolo::MissingDataError, "Version '#{version_to_release}' of title '#{title}' has no uploaded .pkg" if vobj.jamf_pkg_file.pix_empty?
1004
+
1005
+ # visible to jamf?
997
1006
  jamf_patch_title(refresh: true)
998
1007
  return if jamf_patch_title.versions.key?(version_to_release)
999
1008
 
@@ -1144,6 +1153,29 @@ module Xolo
1144
1153
  unlock
1145
1154
  end
1146
1155
 
1156
+ # run the autopkg recipe if defined
1157
+ # See Helpers::AutoPkg for more details
1158
+ ############################
1159
+ def run_autopkg_recipe
1160
+ server_app_instance.run_autopkg_recipe self
1161
+ end
1162
+
1163
+ # Re run the autopkg recipe
1164
+ # and re-upload the pkg
1165
+ ############################
1166
+ def rerun_autopkg_recipe
1167
+ log_change msg: "Re-running autopkg recipe '#{autopkg_recipe}' for latest version '#{latest_version}'"
1168
+
1169
+ pkg_src = server_app_instance.run_autopkg_recipe self
1170
+
1171
+ return unless server_app_instance.validate_autopkg_pkg pkg_src, latest_version
1172
+
1173
+ latest_version_obj = version_object latest_version
1174
+ latest_version_obj.pkg_is_from_autopkg = true
1175
+
1176
+ server_app_instance.process_and_upload_autopkg_pkg title, latest_version_obj, pkg_src
1177
+ end
1178
+
1147
1179
  # Is this title locked for updates?
1148
1180
  #############################
1149
1181
  def locked?
@@ -307,8 +307,22 @@ module Xolo
307
307
 
308
308
  auto_release_delay = title_object.auto_release_delay.to_i
309
309
 
310
- # is auto_release_delay an integer?
311
- if title_object.auto_release_delay&.pix_integer? && !auto_release_delay.negative?
310
+ # is auto_release_delay monthly?
311
+ if title_object.auto_release_delay == Xolo::MONTHLY
312
+ monthly = Xolo::Server.config.auto_release_monthly
313
+ day =
314
+ if monthly.pix_integer?
315
+ monthly.to_i.pix_ordinalize
316
+ elsif %w[sun mon tue wed thu fri sat].include? monthly[-3..-1].downcase
317
+ monthly
318
+ else
319
+ '1st'
320
+ end
321
+
322
+ msg = "New pilot for subscribed title '#{title}', version '#{new_version}' has been created in Xolo via subscription.\nIt will be released during nightly maintenance on the next #{day} of the month"
323
+
324
+ # is auto_release_delay an integer?
325
+ elsif title_object.auto_release_delay&.pix_integer? && !auto_release_delay.negative?
312
326
  how_soon =
313
327
  if auto_release_delay.zero?
314
328
  'tonight'
@@ -696,8 +710,12 @@ module Xolo
696
710
  progress 'Saving version data to Xolo server'
697
711
  save_local_data
698
712
 
699
- create_patch_in_ted unless subscribed?
713
+ # prepend our version to the version_order array of the title once
714
+ # our version data has been saved at least once.
715
+ progress "Updating title version_order, prepending '#{version}'", log: :info
716
+ title_object.prepend_version(version)
700
717
 
718
+ create_patch_in_ted unless subscribed?
701
719
  create_in_jamf
702
720
 
703
721
  self.status = STATUS_PILOT
@@ -706,10 +724,6 @@ module Xolo
706
724
  # add some data
707
725
  save_local_data
708
726
 
709
- # prepend our version to the version_order array of the title
710
- progress "Updating title version_order, prepending '#{version}'", log: :info
711
- title_object.prepend_version(version)
712
-
713
727
  log_change msg: 'Version Created'
714
728
 
715
729
  progress "Version '#{version}' of Title '#{title}' has been created in Xolo.", log: :info
@@ -748,18 +762,7 @@ module Xolo
748
762
 
749
763
  pkg_src = title_object.run_autopkg_recipe
750
764
 
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
765
+ return unless server_app_instance.validate_autopkg_pkg pkg_src, version
763
766
 
764
767
  # this lets future code know that the pkg we're working with came from autopkg
765
768
  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.3.0
4
+ version: 2.5.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-09-04 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday