xolo-server 1.0.1 → 2.0.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/data/client/xolo +152 -79
  3. data/lib/xolo/core/base_classes/title.rb +254 -18
  4. data/lib/xolo/core/base_classes/version.rb +47 -7
  5. data/lib/xolo/core/constants.rb +7 -3
  6. data/lib/xolo/core/security_cmd.rb +128 -0
  7. data/lib/xolo/core/version.rb +1 -1
  8. data/lib/xolo/core.rb +1 -0
  9. data/lib/xolo/server/app.rb +7 -0
  10. data/lib/xolo/server/configuration.rb +243 -37
  11. data/lib/xolo/server/constants.rb +10 -0
  12. data/lib/xolo/server/helpers/auth.rb +19 -2
  13. data/lib/xolo/server/helpers/autopkg.rb +157 -0
  14. data/lib/xolo/server/helpers/client_data.rb +90 -60
  15. data/lib/xolo/server/helpers/file_transfers.rb +412 -82
  16. data/lib/xolo/server/helpers/jamf_pro.rb +30 -7
  17. data/lib/xolo/server/helpers/log.rb +2 -0
  18. data/lib/xolo/server/helpers/maintenance.rb +1 -0
  19. data/lib/xolo/server/helpers/notification.rb +4 -3
  20. data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
  21. data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
  22. data/lib/xolo/server/helpers/subscriptions.rb +119 -0
  23. data/lib/xolo/server/helpers/titles.rb +27 -3
  24. data/lib/xolo/server/helpers/versions.rb +23 -11
  25. data/lib/xolo/server/mixins/changelog.rb +9 -16
  26. data/lib/xolo/server/mixins/title_jamf_access.rb +375 -385
  27. data/lib/xolo/server/mixins/title_ted_access.rb +29 -3
  28. data/lib/xolo/server/mixins/version_jamf_access.rb +95 -112
  29. data/lib/xolo/server/mixins/version_ted_access.rb +25 -0
  30. data/lib/xolo/server/object_locks.rb +2 -1
  31. data/lib/xolo/server/routes/auth.rb +2 -2
  32. data/lib/xolo/server/routes/jamf_pro.rb +11 -1
  33. data/lib/xolo/server/routes/maint.rb +2 -1
  34. data/lib/xolo/server/routes/subscriptions.rb +126 -0
  35. data/lib/xolo/server/routes/title_editor.rb +1 -1
  36. data/lib/xolo/server/routes/titles.rb +26 -11
  37. data/lib/xolo/server/routes/uploads.rb +0 -14
  38. data/lib/xolo/server/routes/versions.rb +14 -13
  39. data/lib/xolo/server/routes.rb +9 -0
  40. data/lib/xolo/server/title.rb +100 -77
  41. data/lib/xolo/server/version.rb +177 -15
  42. data/lib/xolo/server.rb +8 -0
  43. metadata +7 -9
@@ -20,6 +20,17 @@ module Xolo
20
20
  #######################
21
21
  #######################
22
22
 
23
+ UPLOAD_ACTION = 'Upload'
24
+ REUPLOAD_ACTION = 'Re-upload'
25
+
26
+ # Thes values in the cdnType field of the Cloud DP definition
27
+ # from the API indicate that there is no Cloud DP configured
28
+ CLOUD_DP_NA = [nil, Xolo::BLANK, 'NONE'].freeze
29
+
30
+ # How long to wait for a pkg to appear on the Cloud DP when uploading via API
31
+ # before giving up and raising an error
32
+ CLOUD_DP_UPLOAD_TIMEOUT = 1800 # seconds - 30 minutes
33
+
23
34
  # Module Methods
24
35
  #######################
25
36
  #######################
@@ -60,7 +71,9 @@ module Xolo
60
71
  log_info "Processing uploaded SelfService icon for #{params[:title]}"
61
72
  title = instantiate_title params[:title]
62
73
  title.save_ssvc_icon(tempfile, filename)
63
- title.configure_pol_for_self_service if title.self_service
74
+ # this will configure the ssvc settings but won't make it available
75
+ # in ssvc - that happens elsewhere.
76
+ title.configure_pol_for_self_service
64
77
  rescue => e
65
78
  msg = "#{e.class}: #{e}"
66
79
  log_error msg
@@ -69,81 +82,211 @@ module Xolo
69
82
  halt 400, { status: 400, error: msg }
70
83
  end
71
84
 
72
- # Handle an uploaded pkg installer
73
- # TODO: wrap this in a thread, it might be very slow for large pkgs.
74
- # TODO: Also, when threaded, how to report errors?
75
- # TODO: Split this into smaller methods
76
- #############################
77
- def process_incoming_pkg
78
- log_info "Processing uploaded installer package for version '#{params[:version]}' of title '#{params[:title]}'"
85
+ # Upload an pkg installer from xadm to Jamf Pro,
86
+ # and do all the processing around that
87
+ ######################
88
+ def process_and_upload_uploaded_pkg
89
+ process_and_upload_to_jamf(
90
+ params[:title],
91
+ params[:version],
92
+ pkg_src: params[:file][:tempfile].path,
93
+ orig_filename: params[:file][:filename]
94
+ )
95
+ end
79
96
 
80
- # the Xolo::Server::Version that owns this pkg
81
- version = instantiate_version title: params[:title], version: params[:version]
82
- version.lock
97
+ # upload a package from autopkg to Jamf Pro
98
+ # and do all the processing around that
99
+ #
100
+ # @param title [String] The title the package belongs to
101
+ # @param version [String, Xolo::Server::Version] the version string or object
102
+ # @param pkg_src [String, Pathname] the path to the pkg
103
+ # @return [void]
104
+ ###########################################
105
+ def process_and_upload_autopkg_pkg(title, version, pkg_src)
106
+ process_and_upload_to_jamf(
107
+ title,
108
+ version,
109
+ pkg_src: pkg_src
110
+ )
111
+ end
83
112
 
84
- # is this a re-upload? True if upload_date as any value in it
85
- if version.upload_date.pix_empty?
86
- action = 'Uploading'
87
- re_uploading = false
88
- else
89
- re_uploading = true
90
- action = 'Re-uploading'
91
- version.log_change msg: 'Re-uploading pkg file'
92
- end
113
+ # Process a pkg installer and upload to jamf
114
+ #
115
+ # @param title [String] the title name
116
+ # @param version [String, Xolo::Server::Version] the version string or object
117
+ # @param pkg_src [String, Pathname] the path to the file to be uploaded to Jamf
118
+ # This could be one uploaded from xadm, or one created by autopkg
119
+ # @param orig_filename [String, nil] the original filename of the pkg, e.g. as uploaded from
120
+ # an admin's computer. If not provided, the basename of pkg_src will be used.
121
+ # @return [void]
122
+ #############################
123
+ def process_and_upload_to_jamf(title, version, pkg_src:, orig_filename: nil)
124
+ pkg_src = Pathname.new pkg_src
125
+ orig_filename ||= pkg_src.basename.to_s
93
126
 
94
- # the original uploaded filename
95
- orig_filename = params[:file][:filename]
96
- log_debug "Incoming pkg file '#{orig_filename}' "
97
- file_extname = validate_uploaded_pkg(orig_filename)
127
+ version = instantiate_version(title: title, version: version) if version.is_a?(String)
98
128
 
99
- # Set the jamf_pkg_file, now that we know the extension
100
- uploaded_pkg_name = "#{version.jamf_pkg_name}#{file_extname}"
101
- log_debug "Jamf: Package filename will be '#{uploaded_pkg_name}'"
102
- version.jamf_pkg_file = uploaded_pkg_name
129
+ staged_pkg = prep_pkg_for_upload(version, pkg_src)
130
+ upload_pkg_in_thread(version, staged_pkg, orig_filename)
131
+ rescue => e
132
+ msg = "#{e.class}: #{e}"
133
+ log_error msg
134
+ e.backtrace.each { |line| log_error "..#{line}" }
135
+ halt 400, { status: 400, error: msg }
136
+ ensure
137
+ pkg_src.delete if pkg_src.file?
138
+ end
103
139
 
104
- # The tempfile created by Sinatra when the pkg was uploaded from xadm
105
- tempfile = Pathname.new params[:file][:tempfile].path
140
+ # Prep a .pkg before we start uploading to the dist point
141
+ #
142
+ # @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
143
+ # @param pkg_src [Pathname] the path to the file to be uploaded to Jamf
144
+ # @return [Pathname] the path to the staged pkg that is ready to be uploaded to Jamf
145
+ #########################################
146
+ def prep_pkg_for_upload(version, pkg_src)
147
+ msg = "Jamf: Processing installer package '#{pkg_src}' (#{pkg_src.size.pix_humanize_bytes}) for Jamf Dist upload, title '#{version.title}' version '#{version.version}'"
148
+ progress msg, log: :info
149
+
150
+ version.jamf_pkg_file = dist_pkg_filename(version)
151
+ log_debug "Jamf: Uploaded package filename will be '#{version.jamf_pkg_file}'"
152
+ version.save_local_data
106
153
 
107
- # The uploaded tmpfile will be staged here before uploading again to
108
- # the Jamf Dist Point(s)
109
- staged_pkg = Xolo::Server::Title.title_dir(params[:title]) + uploaded_pkg_name
154
+ # The pkg_src will be staged here before uploading to the Dist Point
155
+ staged_pkg = Xolo::Server::Title.title_dir(version.title) + version.jamf_pkg_file
110
156
 
111
157
  # remove any old one that might be there
112
158
  staged_pkg.delete if staged_pkg.file?
113
159
 
114
- if need_to_sign?(tempfile)
115
- # This will put the signed pkg into the staged_pkg location
116
- sign_uploaded_pkg(tempfile, staged_pkg)
117
- log_debug "Signing complete, deleting temp file '#{tempfile}'"
118
- tempfile.delete if tempfile.file?
119
- else
120
- log_debug "Uploaded .pkg file doesn't need signing, moving tempfile to '#{staged_pkg.basename}'"
121
- # Put the signed pkg into the staged_pkg location
122
- tempfile.rename staged_pkg
123
- end
160
+ # This will move/copy the pkg_src into the staged_pkg, signing it on the way if needed, and
161
+ # delete the original pkg_src file.
162
+ sign_and_stage(pkg_src, staged_pkg, version)
124
163
 
125
- # upload the pkg with the uploader tool defined in config
126
- # This will set the checksum and manifest in the JPackage object
127
- upload_to_dist_point(version.jamf_package, staged_pkg)
164
+ # Wrap component pkgs in a Distribution pkg if configured to do so
165
+ staged_pkg = wrap_component_pkg_in_distribution(staged_pkg, version) if Xolo::Server.config.create_distribution_pkgs
128
166
 
129
- if re_uploading
130
- # These must be set before calling wait_to_enable_reinstall_policy
131
- version.reupload_date = Time.now
132
- version.reuploaded_by = session[:admin]
167
+ staged_pkg
168
+ end
133
169
 
134
- # This will make the version start a thread
135
- # that will wait some period of time (to allow for pkg uploads
136
- # to complete) before enabling the reinstall policy
137
- version.wait_to_enable_reinstall_policy
138
- else
139
- version.upload_date = Time.now
140
- version.uploaded_by = session[:admin]
170
+ # upload a prepped/staged pkg in a thread, and do the things that need to be done after upload,
171
+ # like setting the upload/reupload date and user, enabling the reinstall policy if it's a reupload, etc
172
+ #
173
+ # @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
174
+ # @param staged_pkg [Pathname] the path to the staged pkg that is ready to be uploaded to Jamf
175
+ # @return [void]
176
+ #####################################
177
+ def upload_pkg_in_thread(version, staged_pkg, orig_filename)
178
+ if @pkg_upload_thread&.alive?
179
+ msg = "A pkg upload is already in progress for version '#{version}' - can't start another one until it's done"
180
+ log_error msg
181
+ raise msg
141
182
  end
142
183
 
184
+ @pkg_upload_thread = Thread.new do
185
+ begin
186
+ # is this a re-upload? The jamf_pkg_file will have already
187
+ # been updated to reflect the new filename with the _N_ if it's a re-upload
188
+ re_uploading = version.jamf_pkg_file =~ /_(\d+)_\.pkg$/
189
+
190
+ # disable reinstall policy if re-uploading,
191
+ # will be re-enabled after the upload
192
+ if re_uploading
193
+ action = REUPLOAD_ACTION
194
+ pol = version.jamf_auto_reinstall_policy
195
+ pol.disable
196
+ pol.save
197
+ else
198
+ action = UPLOAD_ACTION
199
+ end
200
+
201
+ upload_to_dist_point(version.jamf_package, staged_pkg)
202
+
203
+ uploaded_by =
204
+ if version.title_object.autopkg_enabled?
205
+ Xolo::Server::Helpers::AutoPkg::AUTOPKG_UPLOADED_BY
206
+ elsif defined?(session)
207
+ session[:admin]
208
+ end
209
+
210
+ if re_uploading
211
+ version.reupload_date = Time.now
212
+ version.reuploaded_by = uploaded_by
213
+
214
+ # if upload via API, wait for pkg to appear then enable the policy
215
+ if upload_via_api?
216
+ wait_for_pkg_and_enable_reinstall_policy(version)
217
+
218
+ # otherwise notify someone to confirm upload is complete before enabling the policy
219
+ else
220
+ msg = "Please confirm that re-uploaded pkg '#{version.jamf_pkg_file}' is on the dist point and ready to go, then enable the reinstall policy '#{pol.name}' at #{jamf_auto_reinstall_policy_url}"
221
+ log_info msg, alert: true
222
+
223
+ end # if upload_via_api?
224
+
225
+ # update the dist filename in the jamf package object
226
+ version.jamf_package.fileName = version.jamf_pkg_file
227
+ version.jamf_package.packageName = version.jamf_pkg_name
228
+ version.jamf_package.save
229
+
230
+ # if this is a first-time upload, just set the upload date and user
231
+ else
232
+ version.upload_date = Time.now
233
+ version.uploaded_by = uploaded_by
234
+ end # if re_uploading
235
+ version.save_local_data
236
+ version.log_change msg: "#{action}ed pkg file '#{staged_pkg.basename}' to Jamf Pro dist point(s)"
237
+ rescue => e
238
+ msg = "Error in pkg upload thread: #{e.class}: #{e}"
239
+ log_error msg
240
+ e.backtrace.each { |line| log_error "..#{line}" }
241
+ end # begin
242
+
243
+ update_version_post_upload(version, staged_pkg, orig_filename)
244
+ end # thread
245
+ end
246
+
247
+ # Wait for a re-uploaded pkg to appear on the Cloud DP after an API upload,
248
+ # then enable the reinstall policy
249
+ #
250
+ # @param version [Xolo::Server::Version] the version that is being re-uploaded
251
+ # @return [void]
252
+ #####################
253
+ def wait_for_pkg_and_enable_reinstall_policy(version)
254
+ start_time = Time.now
255
+
256
+ until cloud_dp_pkg_ready?(version.jamf_pkg_file)
257
+ if Time.now - start_time > CLOUD_DP_UPLOAD_TIMEOUT
258
+ msg = "Timed out waiting for pkg '#{version.jamf_pkg_file}' to appear on Cloud DP after upload via API"
259
+ log_error msg
260
+ raise msg
261
+ end
262
+
263
+ log_debug "Checking every minute for pkg '#{version.jamf_pkg_file}' to appear on Cloud DP after upload via API..."
264
+ sleep 60
265
+ end # until
266
+
267
+ log_debug "Pkg '#{version.jamf_pkg_file}' is now on Cloud DP, enabling reinstall policy"
268
+ pol = version.jamf_auto_reinstall_policy
269
+ pol.enable
270
+ pol.save
271
+
272
+ msg = "Re-uploaded pkg '#{version.jamf_pkg_file}' is on the dist point and ready to go, reinstall policy '#{pol.name}' has been enabled"
273
+ log_info msg, alert: true
274
+ end
275
+
276
+ # After uploading a pkg, update the version with info about the pkg,
277
+ # like whether it's a dist pkg or not, and save the manifest and checksum
278
+ # NOTE this is run as part of the upload thread.
279
+ #
280
+ # @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
281
+ # @param staged_pkg [Pathname] the path to the staged pkg that was uploaded to Jamf
282
+ # @return [void]
283
+ ##############################
284
+ def update_version_post_upload(version, staged_pkg, orig_filename)
143
285
  # make note if the pkg is a Distribution package
144
286
  version.dist_pkg = pkg_is_distribution?(staged_pkg)
145
287
 
146
- # save the manifest just in case
288
+ # save the manifest on the server, just in case
289
+ # TODO: Support sha3_512 in manifests
147
290
  version.manifest_file.pix_atomic_write(version.jamf_package.manifest)
148
291
 
149
292
  # save the checksum just in case
@@ -156,18 +299,59 @@ module Xolo
156
299
  version.save_local_data
157
300
 
158
301
  # log the upload
159
- version.log_change msg: "#{action} pkg file '#{staged_pkg.basename}'"
160
-
161
- # remove the staged pkg and the tempfile
162
- staged_pkg.delete
163
- tempfile.delete if tempfile.file?
164
- rescue => e
165
- msg = "#{e.class}: #{e}"
166
- log_error msg
167
- e.backtrace.each { |line| log_error "..#{line}" }
168
- halt 400, { status: 400, error: msg }
302
+ version.log_change msg: "Uploaded pkg file '#{staged_pkg.basename}' to dist point"
169
303
  ensure
170
- version.unlock
304
+ staged_pkg.delete if staged_pkg.file?
305
+ end
306
+
307
+ # What will be the name of the file on the dist point?
308
+ # For a first upload, it will be 'xolo-<title>-<version>.pkg'
309
+ #
310
+ # If we are re-uploading, it will be 'xolo-<title>-<version>_N_.pkg'
311
+ # where N is an integer (starting with 2) that increments with each re-upload,
312
+ #
313
+ # This is so that re-uploads don't have the same filename on the Dist Point, which could
314
+ # be problematic. It also helps to visually see that this is a re-uploaded pkg
315
+ # and not the original one.
316
+ #
317
+ # @param version [Xolo::Server::Version] the version that is being re-uploaded
318
+ #
319
+ # @return [String] the filename to use for the pkg on the dist point
320
+ ####################
321
+ def dist_pkg_filename(version)
322
+ if version.upload_date.pix_empty?
323
+ # no upload date, this is the first upload
324
+ "#{version.jamf_pkg_name}#{Xolo::DOT_PKG}"
325
+
326
+ elsif version.jamf_pkg_file.to_s =~ /_(\d+)_\.pkg$/
327
+ # this is a re-upload, does the filename indicat a previous re-upload with a _N_ in the name?
328
+ next_num = Regexp.last_match[1].to_i + 1
329
+ "#{version.jamf_pkg_name}_#{next_num}_#{Xolo::DOT_PKG}"
330
+
331
+ else
332
+ # the first re-upload, just add _2_ before the extension
333
+ "#{version.jamf_pkg_name}_2_#{Xolo::DOT_PKG}"
334
+ end
335
+ end
336
+
337
+ # If this pkg needs signing, do so putting the signed pkg in the staged_pkg location,
338
+ # and delete the original pkg_src file.
339
+ # If it doesn't need signing, just move it to the staged_pkg location.
340
+ #
341
+ # @param pkg_src [Pathname] the path to the file to be uploaded to Jamf
342
+ # @param staged_pkg [Pathname] the path where the pkg should be staged for upload to Jamf
343
+ # @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
344
+ # @return [void]
345
+ def sign_and_stage(pkg_src, staged_pkg, version)
346
+ if need_to_sign?(pkg_src, version)
347
+ # This will put the signed pkg into the staged_pkg location
348
+ sign_pkg(pkg_src, staged_pkg)
349
+ log_debug "Signing complete, signed pkg is '#{staged_pkg}', deleting original file '#{pkg_src}'"
350
+ pkg_src.delete if pkg_src.file?
351
+ else
352
+ log_debug "The .pkg file doesn't need signing, moving pkg_src to '#{staged_pkg}'"
353
+ pkg_src.rename staged_pkg
354
+ end
171
355
  end
172
356
 
173
357
  # Check if a package is a Distribution package, if not,
@@ -182,14 +366,69 @@ module Xolo
182
366
  pkg_file = Pathname.new(pkg_file)
183
367
  raise ArgumentError, "pkg_file does not exist or not a file: #{pkg_file}" unless pkg_file.file?
184
368
 
185
- tmpdir = Pathname.new(Dir.mktmpdir)
186
- workdir = tmpdir + "#{pkg_file.basename}-expanded"
369
+ `/usr/bin/xar -tf #{pkg_file.to_s.shellescape}`.split("\n").include? 'Distribution'
370
+ end
187
371
 
188
- system "/usr/sbin/pkgutil --expand #{pkg_file.to_s.shellescape} #{workdir.to_s.shellescape}"
372
+ # Wrap a component pkg in a Distribution pkg, return the path to the Distribution pkg,
373
+ # which should be the same as the orig_pkg
374
+ #
375
+ # @param orig_pkg [Pathname, String] The path to the component .pkg
376
+ # @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
377
+ #
378
+ # @return [Pathname] The path to the new Distribution pkg
379
+ ###########################################
380
+ def wrap_component_pkg_in_distribution(orig_pkg, version)
381
+ orig_pkg = Pathname.new(orig_pkg)
189
382
 
190
- workdir.children.map(&:basename).map(&:to_s).include? 'Distribution'
191
- ensure
192
- tmpdir.rmtree
383
+ raise ArgumentError, "pkg_file does not exist or not a file: #{orig_pkg}" unless orig_pkg.file?
384
+
385
+ if pkg_is_distribution?(orig_pkg)
386
+ log_debug "Package '#{orig_pkg.basename}' is already a Distribution pkg, not wrapping"
387
+ return orig_pkg
388
+ end
389
+
390
+ log_info "Wrapping component pkg '#{orig_pkg.basename}' in a Distribution pkg"
391
+ out_dir = orig_pkg.parent
392
+ out_file = out_dir + "#{orig_pkg.basename(Xolo::DOT_PKG)}_dist#{Xolo::DOT_PKG}"
393
+
394
+ # the productbuild command, with signing if needed
395
+ prodbuild_cmd = +"/usr/bin/productbuild --package #{orig_pkg.to_s.shellescape} "
396
+ signing_reason =
397
+ if version.pkg_is_from_autopkg && Xolo::Server.config.sign_autopkg_pkgs
398
+ 'autopkg'
399
+ elsif !version.pkg_is_from_autopkg && Xolo::Server.config.sign_pkgs
400
+ 'uploaded'
401
+ end
402
+
403
+ if signing_reason
404
+ log_info "Signing is enabled for #{signing_reason} pkgs, will sign the Distribution pkg as part of wrapping process"
405
+
406
+ sh_kch = Shellwords.escape Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
407
+ sh_ident = Shellwords.escape Xolo::Server.config.pkg_signing_identity
408
+ unlock_signing_keychain
409
+ prodbuild_cmd << "--sign #{sh_ident} --keychain #{sh_kch} "
410
+ end
411
+
412
+ prodbuild_cmd << out_file.to_s.shellescape
413
+
414
+ log_debug "Wrapping component pkg in Distribution pkg with this command: #{prodbuild_cmd}"
415
+
416
+ if system prodbuild_cmd
417
+ # remove the component pkg
418
+ orig_pkg.delete
419
+ # rename the dist pkg to the original pkg name,
420
+ out_file.rename orig_pkg
421
+
422
+ return orig_pkg
423
+ end
424
+
425
+ raise "Failed to wrap component pkg '#{orig_pkg.basename}' in a Distribution pkg"
426
+ end
427
+
428
+ # are Dist Point uploads configured to be done via the API, or with an upload tool defined in config?
429
+ #############################
430
+ def upload_via_api?
431
+ Xolo::Server.config.upload_tool.to_s.downcase == 'api'
193
432
  end
194
433
 
195
434
  # upload a staged pkg to the dist point(s)
@@ -201,9 +440,17 @@ module Xolo
201
440
  # @return [void]
202
441
  ###########################################
203
442
  def upload_to_dist_point(jpkg, pkg_file)
204
- if Xolo::Server.config.upload_tool.to_s.downcase == 'api'
205
- jpkg.upload pkg_file # this will update the checksum and manifest automatically, and save back to the server
443
+ # via API
444
+ if upload_via_api?
445
+ log_debug 'Jamf: increasing the API timeout to 30 minutes for the pkg upload'
446
+ jpkg.cnx.jp_cnx.options.timeout = 1800
447
+
448
+ log_debug "Jamf: Attempting upload of #{pkg_file.basename} to primary dist point via API"
449
+ # this will update the checksum and manifest automatically, and save back to the jamf pro server
450
+ jpkg.upload pkg_file
206
451
  log_info "Jamf: Uploaded #{pkg_file.basename} to primary dist point via API, with new checksum and manifest"
452
+
453
+ # via upload tool defined in config
207
454
  else
208
455
  log_debug "Jamf: Regenerating manifest for package '#{jpkg.packageName}' from #{pkg_file.basename}"
209
456
  jpkg.generate_manifest(pkg_file)
@@ -233,19 +480,22 @@ module Xolo
233
480
  cmd = "#{tool} #{jpkg_name} #{pkg}"
234
481
 
235
482
  stdouterr, exit_status = Open3.capture2e(cmd)
236
- return if exit_status.success?
483
+ if exit_status.success?
484
+ log_debug "Jamf: upload tool succeeded in uploading #{pkg_file.basename} to dist point(s)."
485
+ return
486
+ end
237
487
 
238
- msg = "Uploader tool failed to upload #{pkg_file.basename} to dist point(s): #{stdouterr}"
488
+ msg = "Uploader tool failed to upload #{pkg_file.basename} to dist point(s): #{stdouterr}"
239
489
  log_error msg
240
490
  raise msg
241
491
  end
242
492
 
243
493
  # Confirm and return the extension of the originally uplaoded file,
244
- # either .pkg or .zip
494
+ # as .pkg
245
495
  #
246
496
  # @param filename [String] The original name of the file uploaded to Xolo.
247
497
  #
248
- # @return [String] either '.pkg' or '.zip'
498
+ # @return [String] '.pkg' is the only valid one for now
249
499
  ###############################
250
500
  def validate_uploaded_pkg(filename)
251
501
  log_debug "Validating pkg file ext for '#{filename}'"
@@ -253,7 +503,87 @@ module Xolo
253
503
  file_extname = Pathname.new(filename).extname
254
504
  return file_extname if Xolo::OK_PKG_EXTS.include? file_extname
255
505
 
256
- raise "Bad filename '#{filename}'. Package files must end in .pkg or .zip (for old-style bundle packages)"
506
+ raise "Bad filename '#{filename}'. Package files must end in #{Xolo::OK_PKG_EXTS.join(', or ')}"
507
+ end
508
+
509
+ # TODO: Use ruby-jss when it implements could-distribution-point rsrc
510
+ #
511
+ # @return [Hash] The Cloud DP definition from the API, if available, minus the keyPairId & privateKey
512
+ # If no cloud dp defined, returns { cdnType: 'NONE', master: false }
513
+ ####################
514
+ def cloud_dp_data
515
+ return @cloud_dp_data if @cloud_dp_data
516
+
517
+ @cloud_dp_data = jamf_cnx.jp_get '/v1/cloud-distribution-point'
518
+ @cloud_dp_data.delete :privateKey
519
+ @cloud_dp_data.delete :keyPairId
520
+ @cloud_dp_data
521
+ rescue Jamf::Connection::JamfProAPIError => e
522
+ @cloud_dp_data = { cdnType: 'NONE', master: false }
523
+ return @cloud_dp_data if jamf_cnx.last_http_response.status == 404
524
+
525
+ raise e
526
+ end
527
+
528
+ # @return [Boolean] Is a cloud distribution point defined?
529
+ ###############################
530
+ def cloud_dp_available?
531
+ !CLOUD_DP_NA.include? cloud_dp_data[:cdnType]
532
+ end
533
+
534
+ # TODO: Use ruby-jss when it implements could-distribution-point rsrc
535
+ # @return [Boolean] Is a cloud distribution point defined?
536
+ ###############################
537
+ def cloud_dp_principal?
538
+ cloud_dp_available? && cloud_dp_data[:master]
539
+ end
540
+
541
+ # TODO: Use ruby-jss when it implements could-distribution-point rsrc
542
+ #
543
+ # Does a given pkg name exist on the cloud dp with 'ready' status?
544
+ #
545
+ # @param pkg_name [String] the name of the pkg to look for
546
+ #
547
+ # @return [Boolean] Is the pkg ready-to-go on the Cloud DP?
548
+ ###############################
549
+ def cloud_dp_pkg_ready?(pkg_filename)
550
+ return false unless cloud_dp_available?
551
+
552
+ filt = CGI.escape "fileName=='#{pkg_filename}'"
553
+ response = jamf_cnx.jp_get "/v1/cloud-distribution-point/files?filter=#{filt}"
554
+
555
+ # No fileserver I know of will allow multiples of a single filename....
556
+ # so assume there's only zero or one
557
+ data = response[:results].first
558
+ return false unless data
559
+
560
+ # once the status is ready, we should be good to go
561
+ data[:status] == 'READY'
562
+ end
563
+
564
+ # TODO: Use ruby-jss when it implements could-distribution-point rsrc
565
+ #
566
+ # @return [Hash {String => String}] The Jamf ID => FileName of all 'READY' PACKAGE files on
567
+ # the Cloud DP.
568
+ ###############################
569
+ def cloud_dp_pkgs
570
+ page = 0
571
+ page_size = 1000
572
+ pkgs = {}
573
+ loop do
574
+ response = jamf_cnx.jp_get "/v1/cloud-distribution-point/files?page=#{page}&page-size=#{page_size}"
575
+ results = response[:results]
576
+ break if results.empty?
577
+
578
+ results.each do |f|
579
+ next unless f[:type] == 'PACKAGE' && f[:status] == 'READY'
580
+
581
+ # fileObjectId is the Jamf ID of the Package object for this DP file
582
+ pkgs[f[:fileObjectId]] = f[:fileName]
583
+ end
584
+ page += 1
585
+ end
586
+ pkgs
257
587
  end
258
588
 
259
589
  end # FileTransfers
@@ -94,20 +94,30 @@ module Xolo
94
94
 
95
95
  return @jamf_cnx if @jamf_cnx
96
96
 
97
- @jamf_cnx = Jamf::Connection.new(
97
+ cnx_opts = {
98
98
  name: "jamf-pro-cnx-#{Time.now.strftime('%F-%T')}",
99
99
  host: Xolo::Server.config.jamf_hostname,
100
100
  port: Xolo::Server.config.jamf_port,
101
101
  verify_cert: Xolo::Server.config.jamf_verify_cert,
102
102
  ssl_version: Xolo::Server.config.jamf_ssl_version,
103
103
  open_timeout: Xolo::Server.config.jamf_open_timeout,
104
- timeout: Xolo::Server.config.jamf_timeout,
105
- user: Xolo::Server.config.jamf_api_user,
106
- pw: Xolo::Server.config.jamf_api_pw,
107
- keep_alive: false
108
- )
109
- log_debug "Jamf: Connected to Jamf Pro at #{@jamf_cnx.base_url} as user '#{Xolo::Server.config.jamf_api_user}'. KeepAlive: #{@jamf_cnx.keep_alive?}, Expires: #{@jamf_cnx.token.expires}. cnx ID: #{@jamf_cnx.object_id}"
104
+ timeout: Xolo::Server.config.jamf_timeout
105
+ }
110
106
 
107
+ if Xolo::Server.config.jamf_use_api_client
108
+ cnxtype = 'API Client'
109
+ cnx_opts[:client_id] = Xolo::Server.config.jamf_api_user
110
+ cnx_opts[:client_secret] = Xolo::Server.config.jamf_api_pw
111
+ else
112
+ cnxtype = 'User'
113
+ cnx_opts[:user] = Xolo::Server.config.jamf_api_user
114
+ cnx_opts[:pw] = Xolo::Server.config.jamf_api_pw
115
+ end
116
+
117
+ @jamf_cnx = Jamf::Connection.new(**cnx_opts)
118
+
119
+ log_debug "Jamf: Connected to Jamf Pro at #{@jamf_cnx.base_url} as #{cnxtype} '#{Xolo::Server.config.jamf_api_user}'. KeepAlive: false, Expires: #{@jamf_cnx.token.expires}. cnx ID: #{@jamf_cnx.object_id}"
120
+ # log_debug "jamf_cnx caller:\n..#{caller_locations(1, 10).map(&:to_s).join("\n..")}"
111
121
  @jamf_cnx
112
122
  end
113
123
 
@@ -148,6 +158,19 @@ module Xolo
148
158
  end
149
159
  end
150
160
 
161
+ # If we're running on a test server, Jamf objects have a different prefix
162
+ # which is 'xolo-' prod servers and 'xolotest-' on test servers.
163
+ #
164
+ # @return [String] The prefix for Jamf object names
165
+ ###########################
166
+ def jamf_obj_name_pfx_base
167
+ if Xolo::Server.config.test_server
168
+ Xolo::Server::JAMF_TEST_OBJECT_NAME_PFX
169
+ else
170
+ Xolo::Server::JAMF_OBJECT_NAME_PFX
171
+ end
172
+ end
173
+
151
174
  end # JamfPro
152
175
 
153
176
  end # Helpers
@@ -73,12 +73,14 @@ module Xolo
73
73
  ###############################
74
74
  def log_error(msg, alert: false)
75
75
  logger.error(session_svr_obj_id) { msg }
76
+ # caller.each { |l| logger.error(session_svr_obj_id) { "..#{l}" } }
76
77
  send_alert msg, :ERROR if alert
77
78
  end
78
79
 
79
80
  ###############################
80
81
  def log_fatal(msg, alert: false)
81
82
  logger.fatal(session_svr_obj_id) { msg }
83
+ caller.each { |l| logger.fatal(session_svr_obj_id) { "..#{l}" } }
82
84
  send_alert msg, :FATAL if alert
83
85
  end
84
86
 
@@ -175,6 +175,7 @@ module Xolo
175
175
  # TODO: Be DRY with this stuff and similar in title_jamf_access.rb
176
176
  Xolo::Server::Title.all_titles.each do |title|
177
177
  title_obj = instantiate_title title
178
+ next if title_obj.subscribed?
178
179
  next unless title_obj.jamf_patch_ea_awaiting_acceptance?
179
180
 
180
181
  log_info "Cleanup: Auto-accepting Title Editor EA for title '#{title}'"